Типы
Системы типов традиционно делятся на два разных лагеря: статические системы типов, где каждый выражение программы должно иметь тип, вычислимый до выполнения программы, и динамические системы типов, где о типах ничего не известно до времени выполнения, когда доступны фактические значения, обрабатываемые программой. Объектно-ориентированное программирование позволяет некоторую гибкость в статически типизированных языках, позволяя писать код без знания точных типов значений во время компиляции. Способность писать код, который может работать с различными типами, называется полиморфизмом. Весь код в классических динамически типизированных языках является полиморфным: только путем явного проверки типов или при невозможности объектов поддерживать операции во время выполнения, типы каких-либо значений когда-либо ограничиваются.
Система типов Julia является динамической, но получает некоторые преимущества статических систем типов, делая возможным указать, что определенные значения имеют определенные типы. Это может быть очень полезно для генерации эффективного кода, но еще важнее, это позволяет диспетчеризации методов по типам аргументов функций быть глубоко интегрированной в язык. Диспетчеризация методов подробно рассматривается в Методах, но основана на системе типов, представленной здесь.
По умолчанию в Julia, когда типы опущены, значения могут быть любого типа. Таким образом, можно написать много полезных функций Julia, никогда не используя явно типы. Однако, когда требуется дополнительная выразительность, легко постепенно вводить явные аннотации типов в ранее «нетипизированный» код. Добавление аннотаций служит трем основным целям: использовать мощный механизм множественной диспетчеризации Julia, улучшить читаемость кода человеком и обнаруживать ошибки программиста.
Описывая Julia языком систем типов, она является: динамической, номинативной и параметрической. Обобщенные типы могут быть параметризованы, а иерархические отношения между типами явно объявляются, а не предполагаются по совместимой структуре. Одной из особенно отличительных особенностей системы типов Julia является то, что конкретные типы не могут быть подтипами друг друга: все конкретные типы являются конечными и могут иметь только абстрактные типы в качестве своих супертипов. Хотя это на первый взгляд может показаться чрезмерно ограничительным, это имеет много положительных последствий при удивительно небольшом количестве недостатков. Оказывается, что способность наследовать поведение гораздо важнее, чем способность наследовать структуру, а наследование обоих приводит к значительным трудностям в традиционных объектно-ориентированных языках. Другие высокоуровневые аспекты системы типов Julia, которые следует упомянуть, это:
- Нет разделения между объектами и значениями, не являющимися объектами: все значения в Julia являются истинными объектами, имеющими тип, принадлежащий одному полностью связанному графу типов, все узлы которого являются одинаково первоклассными типами.
- Нет никакого осмысленного понятия «типа времени компиляции»: единственный тип значения - это его фактический тип во время выполнения программы. Это называется «типом времени выполнения» в объектно-ориентированных языках, где комбинация статической компиляции с полиморфизмом делает это различие значимым.
- Типы имеют значения, а не переменные - переменные просто имена, связанные со значениями.
- И абстрактные, и конкретные типы могут быть параметризованы другими типами. Они также могут быть параметризованы символами, значениями любого типа, для которого
isbitsвозвращает true (по существу, такие вещи, как числа и булевы значения, которые хранятся как типы C илиstructбез указателей на другие объекты), а также кортежами из них. Параметры типа могут быть опущены, когда они не должны быть сохранены или ограничены.
Система типов Julia разработана для того, чтобы быть мощной и выразительной, но ясной, интуитивной и ненавязчивой. Многие программисты Julia, возможно, никогда не почувствуют необходимости писать код, который явно использует типы. Однако некоторые виды программирования становятся яснее, проще, быстрее и надежнее с объявленными типами.
Объявления типов
Оператор :: может использоваться для добавления аннотаций типов к выражениям и переменным в программах. Есть две основные причины для этого:
- В качестве утверждения, чтобы помочь подтвердить, что ваша программа работает так, как вы ожидаете,
- Для предоставления дополнительной информации о типе компилятору, который затем может улучшить производительность в некоторых случаях
Когда он добавлен к выражению, вычисляющему значение, оператор :: читается как «является экземпляром». Его можно использовать в любом месте, чтобы утверждать, что значение выражения слева является экземпляром типа справа. Когда тип справа является конкретным, значение слева должно иметь этот тип как реализацию - помните, что все конкретные типы являются конечными, поэтому никакая реализация не является подтипом любой другой. Когда тип является абстрактным, достаточно, чтобы значение реализовывалось конкретным типом, который является подтипом абстрактного типа. Если утверждение типа неверно, выбрасывается исключение, в противном случае возвращается значение слева:
julia> (1+2)::AbstractFloat ERROR: TypeError: in typeassert, expected AbstractFloat, got Int64 julia> (1+2)::Int 3
Это позволяет добавить утверждение типа к любому выражению на месте.
Когда он добавляется к переменной слева от присваивания или в качестве части объявления local, оператор :: означает нечто немного иное: он объявляет переменную как всегда имеющую указанный тип, как объявление типа в статически типизированном языке, таком как C. Каждое присвоенное переменной значение будет преобразовано в объявленный тип с использованием convert:
julia> function foo()
x::Int8 = 100
x
end
foo (generic function with 1 method)
julia> foo()
100
julia> typeof(ans)
Int8
Эта функция полезна для избежания проблем с производительностью, которые могут возникнуть, если одно из присваиваний переменной неожиданно изменит ее тип.
Это поведение «объявления» происходит только в определенных контекстах:
local x::Int8 # in a local declaration x::Int8 = 10 # as the left-hand side of an assignment
и применяется ко всему текущему пространству имен, даже до объявления. В настоящее время объявления типов не могут использоваться в глобальной области, например, в REPL, так как в Julia еще нет глобальных констант-типов.
Объявления также могут быть присоединены к определениям функций:
function sinc(x)::Float64
if x == 0
return 1
end
return sin(pi*x)/(pi*x)
end
Возврат из этой функции ведет себя так же, как и присваивание переменной с объявленным типом: значение всегда преобразуется в Float64.
Абстрактные типы
Абстрактные типы не могут быть созданы, и служат только узлами в графе типов, тем самым описывая множества связанных конкретных типов: те конкретные типы, которые являются их потомками. Мы начинаем с абстрактных типов, даже если у них нет экземпляров, потому что они являются основой системы типов: они образуют концептуальную иерархию, которая делает систему типов Julia больше, чем просто коллекцию реализаций объектов.
Вспомните, что в Целых и числах с плавающей точкой мы представили различные конкретные типы числовых значений: Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32 и Float64. Хотя они имеют разные размеры представления, Int8, Int16, Int32, Int64 и Int128 все являются знаковыми целочисленными типами. Аналогично, UInt8, UInt16, UInt32, UInt64 и UInt128 - все беззнаковые целочисленные типы, а Float16, Float32 и Float64 отличаются тем, что являются типами с плавающей точкой, а не целыми числами. Часто кусок кода имеет смысл, например, только если его аргументы являются целыми числами, но фактически не зависит от того, какого именно типа целое число. Например, алгоритм наибольшего общего делителя работает для всех типов целых чисел, но не будет работать для чисел с плавающей точкой. Абстрактные типы позволяют создавать иерархию типов, предоставляя контекст, в который могут вписываться конкретные типы. Это позволяет вам, например, легко программировать для любого типа, являющегося целым числом, без ограничения алгоритма конкретным типом целого числа.
Абстрактные типы объявляются с помощью ключевого слова abstract type. Общие синтаксические конструкции для объявления абстрактного типа:
abstract type «name» end abstract type «name» <: «supertype» end
Ключевое слово abstract type вводит новый абстрактный тип, имя которого задается «name». Это имя может быть необязательно после <: и уже существующего типа, указывающего, что только что объявленный абстрактный тип является подтипом этого «родительского» типа.
Если супертип не указан, по умолчанию супертипом является Any - предопределенный абстрактный тип, экземпляром которого являются все объекты, и подтипом которого являются все типы. В теории типов Any обычно называется «верхним», потому что он находится на вершине графа типов. Julia также имеет предопределенный абстрактный «нижний» тип, находящийся в нижней части графа типов, который записывается как Union{}. Он является точной противоположностью Any: ни один объект не является экземпляром Union{} и все типы являются супертипами Union{}.
Давайте рассмотрим некоторые из абстрактных типов, составляющих числовую иерархию Julia:
abstract type Number end abstract type Real <: Number end abstract type AbstractFloat <: Real end abstract type Integer <: Real end abstract type Signed <: Integer end abstract type Unsigned <: Integer end
Тип Number является прямым дочерним типом Any, а Real — его дочерним типом. В свою очередь, Real имеет двух дочерних типов (хотя их больше; остальные рассмотрим позже): Integer и AbstractFloat, разделяя мир на представления целых и вещественных чисел. Представления вещественных чисел, конечно же, включают типы с плавающей точкой, а также другие типы, такие как рациональные числа. Следовательно, AbstractFloat является правильным подтипом Real, включающим только представления вещественных чисел с плавающей точкой. Целые числа далее подразделяются на типы Signed и Unsigned.
Оператор <: в общем случае означает "является подтипом", а в объявлениях такого вида объявляет тип справа непосредственным супертипом вновь объявленного типа. Он также может использоваться в выражениях как оператор подтипов, возвращающий true при условии, что левый операнд является подтипом правого операнда:
julia> Integer <: Number true julia> Integer <: AbstractFloat false
Важное применение абстрактных типов — предоставление реализаций по умолчанию для конкретных типов. В качестве простого примера рассмотрим:
function myplus(x,y)
x+y
end
Первое, что следует отметить, — что объявления аргументов выше эквивалентны x::Any и y::Any. При вызове этой функции, скажем, как myplus(2,5), диспетчер выбирает наиболее конкретный метод с именем myplus, который соответствует заданным аргументам. (Дополнительную информацию о множественной диспетчеризации см. в разделе Методы.)
Если метод, более конкретный, чем указанный выше, не найден, Julia затем внутренне определяет и компилирует метод с именем myplus специально для двух Int аргументов, основанный на общем методе, указанном выше, т.е. он неявно определяет и компилирует:
function myplus(x::Int,y::Int)
x+y
end
и, наконец, вызывает этот конкретный метод.
Таким образом, абстрактные типы позволяют программистам писать общие функции, которые впоследствии могут использоваться в качестве метода по умолчанию для многих комбинаций конкретных типов. Благодаря множественной диспетчеризации программист полностью контролирует, используется метод по умолчанию или более конкретный метод.
Важно отметить, что нет потерь производительности, если программист полагается на функцию, аргументы которой являются абстрактными типами, поскольку она перекомпилируется для каждой пары аргументов конкретных типов, с которыми она вызывается. (Однако могут возникнуть проблемы с производительностью в случае аргументов функций, которые являются контейнерами абстрактных типов; см. Рекомендации по производительности.)
Примитивные типы
Примитивный тип — это конкретный тип, данные которого состоят из обычных битов. Классическими примерами примитивных типов являются целые числа и значения с плавающей точкой. В отличие от большинства языков, Julia позволяет объявлять собственные примитивные типы, а не предоставлять только фиксированный набор встроенных типов. Фактически, все стандартные примитивные типы определены в самом языке:
primitive type Float16 <: AbstractFloat 16 end primitive type Float32 <: AbstractFloat 32 end primitive type Float64 <: AbstractFloat 64 end primitive type Bool <: Integer 8 end primitive type Char <: AbstractChar 32 end primitive type Int8 <: Signed 8 end primitive type UInt8 <: Unsigned 8 end primitive type Int16 <: Signed 16 end primitive type UInt16 <: Unsigned 16 end primitive type Int32 <: Signed 32 end primitive type UInt32 <: Unsigned 32 end primitive type Int64 <: Signed 64 end primitive type UInt64 <: Unsigned 64 end primitive type Int128 <: Signed 128 end primitive type UInt128 <: Unsigned 128 end
Общие синтаксисы для объявления примитивного типа:
primitive type «name» «bits» end primitive type «name» <: «supertype» «bits» end
Количество битов указывает, сколько памяти требуется для типа, а имя присваивает новое имя типу. Примитивный тип может быть необязательно объявлен как подтип некоторого супертипа. Если супертип опущен, то тип по умолчанию имеет Any в качестве непосредственного супертипа. Объявление Bool выше, следовательно, означает, что для хранения булевого значения требуется восемь битов, а непосредственным супертипом является Integer. В настоящее время поддерживаются только размеры, кратные 8 битам. Поэтому булевы значения, хотя они действительно нуждаются всего лишь в одном бите, не могут быть объявлены меньше восьми битов.
Типы Bool, Int8 и UInt8 имеют одинаковое представление: они представляют собой восьмибитные блоки памяти. Однако, поскольку система типов Julia номинативная, они не взаимозаменяемы, несмотря на то, что имеют одинаковую структуру. Фундаментальное различие заключается в том, что у них разные супертипы: непосредственный супертип Bool — Integer, Int8 — Signed, а UInt8 — Unsigned. Все остальные различия между Bool, Int8 и UInt8 являются вопросами поведения — того, как функции определены для работы с объектами этих типов в качестве аргументов. Вот почему необходима номинативная система типов: если структура определяла тип, который, в свою очередь, определял поведение, то было бы невозможно заставить Bool вести себя иначе, чем Int8 или UInt8.
Составные типы
Составные типы называются записями, структурами или объектами в различных языках. Составной тип — это набор именованных полей, экземпляр которых может рассматриваться как одно значение. Во многих языках составные типы являются единственным видом определяемых пользователем типов, и они являются, пожалуй, наиболее часто используемым определяемым пользователем типом в Julia.
В распространённых объектно-ориентированных языках, таких как C++, Java, Python и Ruby, составные типы также имеют связанные с ними именованные функции, и эта комбинация называется "объектом". В более чистых объектно-ориентированных языках, таких как Ruby или Smalltalk, все значения являются объектами, независимо от того, являются ли они составными или нет. В менее чистых объектно-ориентированных языках, включая C++ и Java, некоторые значения, такие как целые и вещественные числа, не являются объектами, тогда как экземпляры определяемых пользователем составных типов являются истинными объектами с связанными методами. В Julia все значения являются объектами, но функции не связаны с объектами, над которыми они работают. Это необходимо, поскольку Julia выбирает, какой метод функции использовать, с помощью множественной диспетчеризации, что означает, что при выборе метода учитываются типы *всех* аргументов функции, а не только первого (см. Методы для получения дополнительной информации о методах и диспетчеризации). Таким образом, было бы неуместно, чтобы функции "принадлежали" только своему первому аргументу. Организация методов в объектах функций, а не именованные наборы методов "внутри" каждого объекта, в конечном итоге становится очень полезной особенностью дизайна языка.
Составные типы вводятся с помощью ключевого слова struct, за которым следует блок имён полей, необязательно снабжённых типами с помощью оператора :::
julia> struct Foo
bar
baz::Int
qux::Float64
end
Поля без аннотации типа по умолчанию имеют тип Any, и соответственно могут содержать любые типы значений.
Новые объекты типа Foo создаются путём применения объекта типа Foo как функции к значениям для его полей:
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)
julia> typeof(foo)
Foo
Когда тип применяется как функция, он называется *конструктором*. Автоматически генерируются два конструктора (они называются *конструкторами по умолчанию*). Один принимает любые аргументы и вызывает convert для преобразования их в типы полей, а другой принимает аргументы, которые точно соответствуют типам полей. Обе эти функции генерируются, поскольку это упрощает добавление новых определений без непреднамеренного замещения конструктора по умолчанию.
Так как поле bar не ограничено типом, подойдёт любое значение. Однако значение для baz должно быть преобразуемо в Int:
julia> Foo((), 23.5, 1) ERROR: InexactError: Int64(23.5) Stacktrace: [...]
Список имён полей можно получить, используя функцию fieldnames.
julia> fieldnames(Foo) (:bar, :baz, :qux)
Вы можете получить доступ к значениям полей составного объекта, используя традиционную нотацию foo.bar:
julia> foo.bar "Hello, world." julia> foo.baz 23 julia> foo.qux 1.5
Составные объекты, объявленные с помощью struct , являются *неизменяемыми*; их нельзя изменять после создания. Это может показаться странным на первый взгляд, но имеет несколько преимуществ:
- Это может быть более эффективным. Некоторые структуры могут быть эффективно упакованы в массивы, и в некоторых случаях компилятор может вообще избежать выделения неизменяемых объектов.
- Невозможно нарушить инварианты, предоставленные конструкторами типа.
- Код, использующий неизменяемые объекты, может быть легче для понимания.
Неизменяемый объект может содержать изменяемые объекты, такие как массивы, в качестве полей. Эти содержащиеся объекты останутся изменяемыми; только поля самого неизменяемого объекта не могут быть изменены для указания на другие объекты.
В случае необходимости изменяемые составные объекты могут быть объявлены с помощью ключевого слова mutable struct, о чём будет сказано в следующей секции.
Неизменяемые составные типы без полей являются одиночными; может быть только один экземпляр таких типов:
julia> struct NoFields
end
julia> NoFields() === NoFields()
true
Функция === подтверждает, что «два» созданных экземпляра NoFields на самом деле являются одним и тем же. Одиночные типы описаны более подробно ниже.
Есть много информации о том, как создаются экземпляры составных типов, но это обсуждение зависит как от Параметрических типов, так и от Методов, и достаточно важно, чтобы обсуждалось в отдельном разделе: Конструкторы.
Изменяемые составные типы
Если составной тип объявлен с mutable struct вместо struct, то его экземпляры можно изменять:
julia> mutable struct Bar
baz
qux::Float64
end
julia> bar = Bar("Hello", 1.5);
julia> bar.qux = 2.0
2.0
julia> bar.baz = 1//2
1//2
Для поддержки мутаций такие объекты обычно выделяются в куче и имеют стабильные адреса памяти. Изменяемый объект подобен маленькому контейнеру, который может содержать различные значения со временем, и поэтому его можно надёжно идентифицировать только по его адресу. Напротив, экземпляр неизменяемого типа связан со специфическими значениями полей — сами значения полей полностью описывают объект. При принятии решения о том, сделать ли тип изменяемым, спросите себя, будут ли два экземпляра с одинаковыми значениями полей считаться идентичными или им потребуется независимо изменяться со временем. Если они будут считаться идентичными, тип, скорее всего, следует сделать неизменяемым.
Для повторения, две ключевые свойства определяют неизменяемость в Julia:
- Не разрешается изменять значение неизменяемого типа.
- Для битовых типов это означает, что битовая структура значения, однажды установленная, никогда не изменится, и это значение является идентификатором битового типа.
- Для составных типов это означает, что идентификаторы значений его полей никогда не изменятся. Когда поля являются битовыми типами, это означает, что их биты никогда не изменятся; для полей, значения которых являются изменяемыми типами, такими как массивы, это означает, что поля всегда будут ссылаться на то же изменяемое значение, даже если содержимое этого изменяемого значения само может быть изменено.
- Объект с неизменяемым типом может быть свободно скопирован компилятором, поскольку его неизменяемость делает невозможным программное различие между исходным объектом и копией.
- В частности, это означает, что достаточно малые неизменяемые значения, такие как целые числа и числа с плавающей точкой, обычно передаются функциям в регистрах (или выделяются в стеке).
- Изменяемые значения, с другой стороны, выделяются в куче и передаются функциям как указатели на значения, выделенные в куче, за исключением случаев, когда компилятор уверен, что нет способа сказать, что это не происходит.
Объявленные типы
Три вида типов (абстрактные, примитивные, составные), обсуждавшиеся в предыдущих разделах, на самом деле тесно связаны. Они разделяют те же ключевые свойства:
- Они объявляются явно.
- У них есть имена.
- У них есть явно объявленные супертипы.
- У них могут быть параметры.
Благодаря этим общим свойствам, эти типы представлены во внутренней форме как экземпляры одной концепции, DataType, которая является типом любого из этих типов:
julia> typeof(Real) DataType julia> typeof(Int) DataType
A DataType может быть абстрактным или конкретным. Если он конкретный, у него есть указанный размер, структура хранения и (необязательно) имена полей. Таким образом, примитивный тип является DataType с ненулевым размером, но без имён полей. Составной тип является DataType, имеющим имена полей или пустым (нулевой размер).
Каждое конкретное значение в системе является экземпляром какого-то DataType.
Объединения типов
Объединение типов — это специальный абстрактный тип, включающий все экземпляры любого из его аргументных типов, построенный с использованием специального ключевого слова Union:
julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got Float64
Компиляторы многих языков имеют внутреннюю конструкцию объединения для рассуждений о типах; Julia просто предоставляет её программисту. Компилятор Julia способен генерировать эффективный код при наличии Union типов с небольшим количеством типов [1], генерируя специализированный код в отдельных ветвях для каждого возможного типа.
Особо полезным случаем Union типа является Union{T, Nothing}, где T может быть любым типом и Nothing — это одиночный тип, единственный экземпляр которого — объект nothing. Этот шаблон является аналогом Julia для Nullable, Option или Maybe типов в других языках. Объявление аргумента функции или поля как Union{T, Nothing} позволяет установить его либо в значение типа T, либо в nothing для обозначения отсутствия значения. Подробнее см. эту запись в FAQ.
Параметрические типы
Важная и мощная особенность системы типов Julia заключается в том, что она параметрическая: типы могут принимать параметры, так что объявления типов фактически вводят целое семейство новых типов — по одному для каждой возможной комбинации значений параметров. Существует много языков, которые поддерживают ту или иную версию обобщённого программирования, где структуры данных и алгоритмы для их обработки могут быть указаны без указания точных типов. Например, некоторая форма обобщённого программирования существует в ML, Haskell, Ada, Eiffel, C++, Java, C#, F# и Scala, чтобы назвать лишь некоторые. Некоторые из этих языков поддерживают истинную параметрическую полиморфию (например, ML, Haskell, Scala), в то время как другие поддерживают специальные, основанные на шаблонах, стили обобщённого программирования (например, C++, Java). С таким многообразием видов обобщённого программирования и параметрических типов в различных языках мы даже не будем пытаться сравнивать параметрические типы Julia с другими языками, а вместо этого сосредоточимся на объяснении системы Julia как таковой. Однако отметим, что, поскольку Julia — динамически типизированный язык и не требует принятия всех решений о типе во время компиляции, многие традиционные трудности, с которыми сталкиваются статические параметрические системы типов, могут быть относительно легко решены.
Все объявленные типы (разновидность DataType ) могут быть параметризованы с одинаковой синтаксической конструкцией в каждом случае. Мы обсудим их в следующем порядке: сначала параметрические составные типы, затем параметрические абстрактные типы и, наконец, параметрические примитивные типы.
Параметрические составные типы
Параметры типа вводятся непосредственно после имени типа, заключенные в фигурные скобки:
julia> struct Point{T}
x::T
y::T
end
Это объявление определяет новый параметрический тип Point{T}, содержащий две «координаты» типа T. Что, спросите вы, такое T? В этом и заключается суть параметрических типов: это может быть любой тип (или значение любого битового типа, хотя здесь он явно используется как тип). Point{Float64} является конкретным типом, эквивалентным типу, определённому путём замены T в определении Point на Float64. Таким образом, это одно объявление фактически объявляет неограниченное количество типов: Point{Float64}, Point{AbstractString}, Point{Int64} и т. д. Каждый из них теперь является используемым конкретным типом:
julia> Point{Float64}
Point{Float64}
julia> Point{AbstractString}
Point{AbstractString}
Тип Point{Float64} представляет точку, координаты которой — значения с плавающей точкой 64-битной точности, а тип Point{AbstractString} представляет «точку», «координаты» которой — строковые объекты (см. Строки).
Point сам по себе также является допустимым объектом типа, содержащим все экземпляры Point{Float64}, Point{AbstractString} и т. д. в качестве подтипов:
julia> Point{Float64} <: Point
true
julia> Point{AbstractString} <: Point
true
Другие типы, конечно, не являются подтипами:
julia> Float64 <: Point false julia> AbstractString <: Point false
Конкретные Point типы с разными значениями T никогда не являются подтипами друг друга:
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
Этот последний момент очень важен: даже если Float64 <: Real , у нас НЕ БУДЕТ Point{Float64} <: Point{Real}.
Другими словами, в терминологии теории типов, параметры типа Julia являются инвариантными, а не ковариантными (или даже контравариантными). Это по практическим причинам: хотя любой экземпляр Point{Float64} концептуально может быть также экземпляром Point{Real}, два типа имеют различное представление в памяти:
- Экземпляр
Point{Float64}может быть представлен компактным и эффективным образом как немедленная пара 64-битных значений; - Экземпляр
Point{Real}должен быть в состоянии хранить любую пару экземпляровReal. Поскольку объекты, являющиеся экземплярамиReal, могут быть произвольного размера и структуры, на практике экземплярPoint{Real}должен быть представлен парой указателей на индивидуально выделенные объектыReal.
Эффективность, полученная за счёт возможности хранения объектов Point{Float64} с непосредственными значениями, многократно возрастает в случае массивов: массив Array{Float64} может храниться как непрерывный блок памяти из 64-битных значений с плавающей точкой, в то время как массив Array{Real} должен быть массивом указателей на индивидуально выделенные объекты Real — которые могут быть упакованными 64-битными значениями с плавающей точкой, но также могут быть произвольно большими и сложными объектами, которые объявлены как реализации абстрактного типа Real.
Так как Point{Float64} не является подтипом Point{Real}, следующий метод не может быть применён к аргументам типа Point{Float64}:
function norm(p::Point{Real})
sqrt(p.x^2 + p.y^2)
end
Правильный способ определения метода, который принимает все аргументы типа Point{T} где T является подтипом Real, таков:
function norm(p::Point{<:Real})
sqrt(p.x^2 + p.y^2)
end
(Аналогично, можно определить function norm(p::Point{T} where T<:Real) или function norm(p::Point{T}) where T<:Real; см. UnionAll Types.)
Дополнительные примеры будут рассмотрены позднее в Методах.
Как создать объект Point? Можно определить пользовательские конструкторы для составных типов, которые будут подробно рассмотрены в Конструкторах, но в отсутствие специальных объявлений конструкторов есть два стандартных способа создания новых составных объектов: один, в котором параметры типа явно задаются, и другой, в котором они подразумеваются аргументами конструктора объекта.
Так как тип Point{Float64} является конкретным типом, эквивалентным Point , объявленному с Float64 вместо T, его можно использовать в качестве конструктора соответственно:
julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
Для конструктора по умолчанию необходимо указать ровно один аргумент для каждого поля:
julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
[...]
julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
[...]
Для параметрических типов генерируется только один конструктор по умолчанию, поскольку его переопределение невозможно. Этот конструктор принимает любые аргументы и преобразует их в типы полей.
Во многих случаях излишне указывать тип объекта Point, который нужно создать, так как типы аргументов вызова конструктора уже неявно предоставляют информацию о типе. По этой причине вы также можете применить Point как конструктор, при условии, что подразумеваемое значение типа параметра T однозначно:
julia> Point(1.0,2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
julia> Point(1,2)
Point{Int64}(1, 2)
julia> typeof(ans)
Point{Int64}
В случае Point, тип T однозначно подразумевается тогда и только тогда, когда два аргумента Point имеют одинаковый тип. Если это не так, конструктор завершится ошибкой MethodError:
julia> Point(1,2.5) ERROR: MethodError: no method matching Point(::Int64, ::Float64) Closest candidates are: Point(::T, !Matched::T) where T at none:2
Методы конструкторов для надлежащего обработки таких смешанных случаев могут быть определены, но это не будет обсуждаться до более позднего момента в разделе Конструкторы.
Параметрические абстрактные типы
Объявления параметрических абстрактных типов объявляют набор абстрактных типов аналогичным образом:
julia> abstract type Pointy{T} end
С этим объявлением Pointy{T} является отдельным абстрактным типом для каждого типа или целого значения T. Как и в случае параметрических составных типов, каждый такой экземпляр является подтипом Pointy:
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
Параметрические абстрактные типы являются инвариантными, так же, как и параметрические составные типы:
julia> Pointy{Float64} <: Pointy{Real}
false
julia> Pointy{Real} <: Pointy{Float64}
false
Запись Pointy{<:Real} может использоваться для выражения аналога Юлии ковариантного типа, в то время как Pointy{>:Int} аналога контравариантного типа, но технически они представляют множества типов (см. UnionAll Типы).
julia> Pointy{Float64} <: Pointy{<:Real}
true
julia> Pointy{Real} <: Pointy{>:Int}
true
Так же, как обычные абстрактные типы служат для создания полезной иерархии типов над конкретными типами, параметрические абстрактные типы служат той же цели в отношении параметрических составных типов. Например, мы могли бы объявить Point{T} подтипом Pointy{T} следующим образом:
julia> struct Point{T} <: Pointy{T}
x::T
y::T
end
С таким объявлением для каждого выбора T, у нас есть Point{T} как подтип Pointy{T}:
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{AbstractString} <: Pointy{AbstractString}
true
Это соотношение также является инвариантным:
julia> Point{Float64} <: Pointy{Real}
false
julia> Point{Float64} <: Pointy{<:Real}
true
Для чего служат параметрические абстрактные типы, такие как Pointy? Представьте, что мы создаем реализацию точки, которая требует только одной координаты, потому что точка находится на диагональной прямой x = y:
julia> struct DiagPoint{T} <: Pointy{T}
x::T
end
Теперь и Point{Float64} и DiagPoint{Float64} являются реализациями абстракции Pointy{Float64}, и аналогично для любого другого возможного выбора типа T. Это позволяет программировать на общем интерфейсе, используемом всеми объектами Pointy, реализованном как для Point, так и для DiagPoint. Однако это нельзя полностью продемонстрировать до тех пор, пока мы не введем методы и диспетчеризацию в следующем разделе, Методы.
Существуют ситуации, когда параметрам типа не следует свободно принимать все возможные типы. В таких ситуациях можно ограничить диапазон T следующим образом:
julia> abstract type Pointy{T<:Real} end
С таким объявлением можно использовать любой тип, являющийся подтипом Real вместо T, но не типы, которые не являются подтипами Real:
julia> Pointy{Float64}
Pointy{Float64}
julia> Pointy{Real}
Pointy{Real}
julia> Pointy{AbstractString}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}
julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Int64
Параметры типа для параметрических составных типов можно ограничить аналогичным образом:
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
Для того чтобы привести реальный пример того, как вся эта параметрическая механика типов может быть полезной, вот фактическое определение неизменяемого типа Юлии Rational (кроме того, мы опускаем здесь конструктор для простоты), представляющего точную дробь целых чисел:
struct Rational{T<:Integer} <: Real
num::T
den::T
end
Это имеет смысл только для взятия отношений целых значений, поэтому тип параметра T ограничен подтипом Integer, а отношение целых чисел представляет собой значение на числовой прямой, поэтому любой Rational является экземпляром абстракции Real.
Кортежи типов
Кортежи — это абстракция аргументов функции — без самой функции. Существенные аспекты аргументов функции — это их порядок и их типы. Поэтому тип кортежа похож на параметрический неизменяемый тип, где каждый параметр — тип одного поля. Например, тип кортежа из 2 элементов похож на следующий неизменяемый тип:
struct Tuple2{A,B}
a::A
b::B
end
Однако есть три ключевых различия:
- Типы кортежей могут иметь любое количество параметров.
- Типы кортежей ковариантны в своих параметрах:
Tuple{Int}является подтипомTuple{Any}. ПоэтомуTuple{Any}считается абстрактным типом, а типы кортежей являются конкретными только если их параметры являются таковыми. - У кортежей нет имен полей; к полям можно получить доступ только по индексу.
Значения кортежей записываются в скобках с запятыми. При построении кортежа соответствующий тип кортежа генерируется по требованию:
julia> typeof((1,"foo",2.5))
Tuple{Int64,String,Float64}
Обратите внимание на последствия ковариантности:
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true
julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false
julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false
Интуитивно это соответствует типу аргументов функции, являющемуся подтипом сигнатуры функции (когда подпись совпадает).
Типы кортежей Vararg
Последний параметр типа кортежа может быть специальным типом Vararg, который обозначает любое количество конечных элементов:
julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString,Vararg{Int64,N} where N}
julia> isa(("1",), mytupletype)
true
julia> isa(("1",1), mytupletype)
true
julia> isa(("1",1,2), mytupletype)
true
julia> isa(("1",1,2,3.0), mytupletype)
false
Заметьте, что Vararg{T} соответствует нулю или более элементов типа T. Типы кортежей Vararg используются для представления аргументов, принимаемых методами varargs (см. Функции Varargs).
Тип Vararg{T,N} соответствует ровно N элементам типа T. NTuple{N,T} является удобным псевдонимом для Tuple{Vararg{T,N}}, т. е. типом кортежа, содержащим ровно N элементов типа T.
Типы именованных кортежей
Именованные кортежи являются экземплярами типа NamedTuple, который имеет два параметра: кортеж символов, дающих имена полей, и тип кортежа, дающий типы полей.
julia> typeof((a=1,b="hello"))
NamedTuple{(:a, :b),Tuple{Int64,String}}
Тип NamedTuple может использоваться как конструктор, принимающий один кортеж аргументов. Построенный тип NamedTuple может быть либо конкретным типом с указанными параметрами, либо типом, который указывает только имена полей:
julia> NamedTuple{(:a, :b),Tuple{Float32, String}}((1,""))
(a = 1.0f0, b = "")
julia> NamedTuple{(:a, :b)}((1,""))
(a = 1, b = "")
Если типы полей указаны, аргументы преобразуются. В противном случае типы аргументов используются непосредственно.
Типы-сингулетоны
Здесь следует упомянуть особый вид параметрического абстрактного типа: типы-сингулетоны. Для каждого типа T, «тип-сингулетон» Type{T} — это абстрактный тип, единственный экземпляр которого — объект T. Поскольку определение немного сложно для анализа, давайте посмотрим на некоторые примеры:
julia> isa(Float64, Type{Float64})
true
julia> isa(Real, Type{Float64})
false
julia> isa(Real, Type{Real})
true
julia> isa(Float64, Type{Real})
false
Другими словами, isa(A,Type{B}) истинно тогда и только тогда, когда A и B — один и тот же объект, и этот объект является типом. Без параметра Type просто абстрактный тип, у которого все объекты типов являются экземплярами, включая, конечно, и типы-сингулетоны:
julia> isa(Type{Float64}, Type)
true
julia> isa(Float64, Type)
true
julia> isa(Real, Type)
true
Любой объект, который не является типом, не является экземпляром Type:
julia> isa(1, Type)
false
julia> isa("foo", Type)
false
До тех пор, пока мы не обсудим Параметрические методы и преобразования, трудно объяснить полезность конструкции типа-сингулетона, но коротко говоря, она позволяет специализировать поведение функций на определённых значениях типов. Это полезно для написания методов (особенно параметрических), поведение которых зависит от типа, заданного как явный аргумент, а не подразумеваемого типом одного из его аргументов.
Некоторые популярные языки программирования имеют типы-сингулетоны, включая Haskell, Scala и Ruby. В общем использовании термин «тип-сингулетон» относится к типу, единственный экземпляр которого — это одно значение. Это значение относится и к типам-сингулетонам Юлии, но с оговоркой, что сингулетонами являются только объекты типов.
Параметрические примитивные типы
Примитивные типы также могут быть объявлены параметрически. Например, указатели представлены как примитивные типы, которые объявляются в Юлии следующим образом:
# 32-bit system:
primitive type Ptr{T} 32 end
# 64-bit system:
primitive type Ptr{T} 64 end
Несколько странная особенность этих объявлений по сравнению с типичными параметрическими составными типами заключается в том, что параметр типа T не используется в определении самого типа — он просто является абстрактной меткой, по существу определяющей целую семью типов с идентичной структурой, отличающихся только параметром типа. Таким образом, Ptr{Float64} и Ptr{Int64} — это разные типы, хотя они имеют идентичные представления. И, конечно же, все конкретные типы указателей являются подтипами универсального типа Ptr:
julia> Ptr{Float64} <: Ptr
true
julia> Ptr{Int64} <: Ptr
true
Типы UnionAll
Мы сказали, что параметрический тип, такой как Ptr действует как супертип всех его экземпляров (Ptr{Int64} и т. д.). Как это работает? Ptr сам по себе не может быть обычным типом данных, так как без знания типа ссылаемых данных тип очевидно не может использоваться для операций с памятью. Ответ заключается в том, что Ptr (или другие параметрические типы, такие как Array ) — это другой вид типа, называемый типом UnionAll. Такой тип выражает повторное объединение типов для всех значений некоторого параметра.
UnionAll типы обычно записываются с использованием ключевого слова where. Например, Ptr можно более точно записать как Ptr{T} where T, что означает все значения, тип которых Ptr{T} для некоторого значения T. В этом контексте параметр T также часто называют «переменной типа», поскольку он похож на переменную, которая пробегает по типам. Каждая where вводит одну переменную типа, поэтому эти выражения вложены для типов с несколькими параметрами, например Array{T,N} where N where T.
Синтаксис применения типа A{B,C} требует, чтобы A был типом UnionAll, и сначала подставляет B для внешней переменной типа в A. Результат ожидается как другой тип UnionAll, в который затем подставляется C. Таким образом, A{B,C} эквивалентно A{B}{C}. Это объясняет, почему можно частично инстанциировать тип, как в Array{Float64}: первое значение параметра было зафиксировано, но второе всё ещё пробегает по всем возможным значениям. Используя явный синтаксис where, можно зафиксировать любой подмножество параметров. Например, тип всех одномерных массивов можно записать как Array{T,1} where T.
Переменные типа могут быть ограничены отношениями подтипов. Array{T} where T<:Integer относится ко всем массивам, тип элементов которых — какой-то вид Integer. Синтаксис Array{<:Integer} — это удобное сокращение для Array{T} where T<:Integer. Переменные типа могут иметь как нижние, так и верхние границы. Array{T} where Int<:T<:Number относится ко всем массивам из Number, которые могут содержать Int (поскольку T должен быть по крайней мере таким же большим, как Int). Синтаксис where T>:Int также используется для указания только нижней границы переменной типа, и Array{>:Int} эквивалентно Array{T} where T>:Int.
Поскольку выражения where вложены, границы переменных типа могут ссылаться на внешние переменные типа. Например, Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real относится к 2-кортежам, первый элемент которых — какое-то Real, а второй — Array любого вида массива, тип элементов которого содержит тип первого элемента кортежа.
Ключевое слово where само по себе может быть вложено внутри более сложного объявления. Например, рассмотрим два типа, созданные следующими объявлениями:
julia> const T1 = Array{Array{T,1} where T, 1}
Array{Array{T,1} where T,1}
julia> const T2 = Array{Array{T,1}, 1} where T
Array{Array{T,1},1} where T
Тип T1 определяет одномерный массив одномерных массивов; каждый из внутренних массивов состоит из объектов одного и того же типа, но этот тип может различаться от одного внутреннего массива к другому. С другой стороны, тип T2 определяет одномерный массив одномерных массивов, все внутренние массивы которых должны иметь один и тот же тип. Обратите внимание, что T2 — это абстрактный тип, например, Array{Array{Int,1},1} <: T2, тогда как T1 — это конкретный тип. Вследствие этого T1 можно создать с конструктором без аргументов a=T1(), но T2 нельзя.
Существует удобный синтаксис для именования таких типов, аналогичный сокращенной форме синтаксиса определения функций:
Vector{T} = Array{T,1}
Это эквивалентно const Vector = Array{T,1} where T. Запись Vector{Float64} эквивалентна записи Array{Float64,1}, и тип «зонтик» Vector включает в себя все Array объекты, где второй параметр — количество измерений массива — равен 1, независимо от того, какой тип элемента. В языках, где параметризованные типы всегда должны быть указаны полностью, это не особенно полезно, но в Julia это позволяет написать просто Vector для абстрактного типа, включающего все одномерные плотные массивы любого типа элемента.
Псевдонимы типов
Иногда удобно ввести новое имя для уже выразимого типа. Это можно сделать с помощью простого оператора присваивания. Например, UInt присвоен псевдоним либо UInt32, либо UInt64, в зависимости от размера указателей на системе:
# 32-bit system: julia> UInt UInt32 # 64-bit system: julia> UInt UInt64
Это достигается с помощью следующего кода в base/boot.jl:
if Int === Int64
const UInt = UInt64
else
const UInt = UInt32
end
Конечно, это зависит от того, чему Int присвоен псевдоним, но это предварительно определённый правильный тип — либо Int32, либо Int64.
(Обратите внимание, что в отличие от Int, Float не существует как псевдоним типа для AbstractFloat определённого размера. В отличие от целочисленных регистров, где размер Int отражает размер родного указателя на данной машине, размеры регистров с плавающей точкой определены стандартом IEEE-754.)
Операции над типами
Поскольку типы в Julia — это сами по себе объекты, обычные функции могут работать с ними. Некоторые функции, которые особенно полезны для работы с типами или изучения типов, уже были введены, например, оператор <:, который указывает, является ли его левый операнд подтипом правого операнда.
Функция isa проверяет, является ли объект данного типа, и возвращает true или false:
julia> isa(1, Int) true julia> isa(1, AbstractFloat) false
Функция typeof, уже используемая на протяжении всего руководства в примерах, возвращает тип своего аргумента. Поскольку, как отмечалось выше, типы — это объекты, у них также есть типы, и мы можем спросить, каковы их типы:
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,String})
Union
Что если мы повторим процесс? Каков тип типа типа? Как оказалось, типы — это составные значения, и, следовательно, у всех них есть тип DataType:
julia> typeof(DataType) DataType julia> typeof(Union) DataType
DataType — это его собственный тип.
Ещё одна операция, которая применяется к некоторым типам, это supertype, которая показывает супертип типа. Только объявленные типы (DataType) имеют недвусмысленные супертипы:
julia> supertype(Float64) AbstractFloat julia> supertype(Number) Any julia> supertype(AbstractString) Any julia> supertype(Any) Any
Если вы примените supertype к другим объектам типов (или объектам, не являющимся типами), будет поднята ошибка MethodError:
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
supertype(!Matched::DataType) at operators.jl:42
supertype(!Matched::UnionAll) at operators.jl:47
Настройка красивого вывода
Часто нужно настроить отображение экземпляров типа. Это достигается перегрузкой функции show. Например, предположим, что мы определяем тип для представления комплексных чисел в полярной форме:
julia> struct Polar{T<:Real} <: Number
r::T
Θ::T
end
julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar
Здесь мы добавили пользовательскую функцию конструктора, чтобы она могла принимать аргументы различных типов Real и преобразовывать их к общему типу (см. Конструкторы и Преобразования и продвижение). (Конечно, нам нужно будет определить и многие другие методы, чтобы он действовал как Number, например, +, *, one, zero, правила продвижения и т. д.) По умолчанию экземпляры этого типа отображаются довольно просто, с информацией о имени типа и значениях полей, как, например, Polar{Float64}(3.0,4.0).
Если мы хотим, чтобы он отображался как 3.0 * exp(4.0im), мы бы определили следующий метод для вывода объекта в заданный объект вывода io (представляющий файл, терминал, буфер и т. д.; см. Сети и потоки):
julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")
Более тонкий контроль над отображением объектов Polar возможен. В частности, иногда требуется как многострочный формат вывода, используемый для отображения отдельного объекта в REPL и других интерактивных средах, так и более компактный однострочный формат, используемый для print или для отображения объекта как части другого объекта (например, в массиве). Хотя по умолчанию функция show(io, z) вызывается в обоих случаях, вы можете определить различный многострочный формат отображения объекта, перегрузив трёхаргументный вариант show, который принимает тип MIME text/plain в качестве второго аргумента (см. Мультимедиа ввода/вывода), например:
julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
print(io, "Polar{$T} complex number:\n ", z)
(Обратите внимание, что print(..., z) здесь вызовет метод show(io, z) с двумя аргументами.) Это приводит к:
julia> Polar(3, 4.0)
Polar{Float64} complex number:
3.0 * exp(4.0im)
julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Array{Polar{Float64},1}:
3.0 * exp(4.0im)
4.0 * exp(5.3im)
где однострочный формат show(io, z) по-прежнему используется для массива значений Polar. Технически, REPL вызывает display(z) для отображения результата выполнения строки, по умолчанию show(stdout, MIME("text/plain"), z), который, в свою очередь, по умолчанию show(stdout, z), но вы не должны определять новые методы display, если не определяете новый обработчик мультимедиа-отображения (см. Мультимедиа ввода/вывода).
Кроме того, вы также можете определить методы show для других типов MIME, чтобы обеспечить более богатое отображение (HTML, изображения и т. д.) объектов в средах, которые поддерживают это (например, IJulia). Например, мы можем определить форматированный вывод HTML для объектов Polar, с надстрочными и курсивными символами, с помощью:
julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
println(io, "<code>Polar{$T}</code> complex number: ",
z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")
Объект Polar затем будет отображаться автоматически с помощью HTML в среде, которая поддерживает отображение HTML, но вы можете вызвать show вручную, чтобы получить HTML-вывод, если хотите:
julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>
HTML-рендерер отобразит это как: Polar{Float64} комплексное число: 3,0 e4,0 i
Как правило, метод show в одну строку должен выводить допустимое выражение Julia для создания показанного объекта. Когда этот show метод содержит инфиксные операторы, такие как оператор умножения (*) в нашем методе show в одну строку для Polar выше, он может не разобрать правильно при выводе как часть другого объекта. Для иллюстрации рассмотрите объект выражения (см. Представление программы), который возводит в квадрат конкретный экземпляр нашего типа Polar.
julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
3.0 * exp(4.0im)
julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2
Поскольку оператор ^ имеет более высокий приоритет, чем * (см. Приоритет операторов и ассоциативность), этот вывод не точно отражает выражение a ^ 2, которое должно быть равно (3.0 * exp(4.0im)) ^ 2. Для решения этой проблемы мы должны создать пользовательский метод для Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int), который вызывается объектом выражения при выводе:
julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
if Base.operator_precedence(:*) <= precedence
print(io, "(")
show(io, z)
print(io, ")")
else
show(io, z)
end
end
julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)
Определенный выше метод добавляет скобки вокруг вызова show когда приоритет вызываемого оператора не ниже приоритета умножения. Эта проверка позволяет выражениям, которые разбираются правильно без скобок (например, :($a + 2) и :($a == 2)), опускать их при выводе:
julia> :($a + 2) :(3.0 * exp(4.0im) + 2) julia> :($a == 2) :(3.0 * exp(4.0im) == 2)
В некоторых случаях полезно настраивать поведение методов show в зависимости от контекста. Это можно сделать с помощью типа IOContext, который позволяет передавать контекстные свойства вместе с обернутым потоком ввода/вывода. Например, мы можем создать более короткое представление в нашем методе show когда свойство :compact установлено в true, возвращаясь к длинному представлению, если свойство false или отсутствует:
julia> function Base.show(io::IO, z::Polar)
if get(io, :compact, false)
print(io, z.r, "ℯ", z.Θ, "im")
else
print(io, z.r, " * exp(", z.Θ, "im)")
end
end
Это новое компактное представление будет использоваться, когда передаваемый поток ввода/вывода является объектом IOContext с установленным свойством :compact. В частности, это происходит при выводе массивов с несколькими столбцами (где ограничено горизонтальное пространство):
julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im
julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Array{Polar{Float64},2}:
3.0ℯ4.0im 4.0ℯ5.3im
См. документацию IOContext для списка общих свойств, которые можно использовать для настройки вывода.
"Типы значений"
В Julia нельзя диспатчить по значению, такому как true или false. Однако вы можете диспатчить по параметрическим типам, и Julia позволяет включать значения «простых битов» (Типы, Символы, Целые числа, числа с плавающей точкой, кортежи и т. д.) в качестве параметров типа. Общий пример — параметр размерности в Array{T,N}, где T является типом (например, Float64), но N просто является Int.
Вы можете создавать собственные пользовательские типы, принимающие значения в качестве параметров, и использовать их для управления диспатчем пользовательских типов. Для иллюстрации этой идеи давайте введём параметрический тип Val{x} и конструктор Val(x) = Val{x}(), который служит обычным способом использования этой техники в случаях, когда вам не нужна более сложная иерархия.
Val определяется как:
julia> struct Val{x}
end
julia> Val(x) = Val{x}()
Val
В реализации Val больше ничего нет. Некоторые функции стандартной библиотеки Julia принимают экземпляры Val в качестве аргументов, и вы также можете использовать их для написания собственных функций. Например:
julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)
julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)
julia> firstlast(Val(true))
"First"
julia> firstlast(Val(false))
"Last"
Для согласованности в Julia вызов всегда должен передавать экземпляр Valэкземпляр, а не тип, то есть использовать foo(Val(:bar)) вместо foo(Val{:bar}).
Стоит отметить, что очень легко неправильно использовать параметрические типы «значений», включая Val; в неблагоприятных случаях вы легко можете значительно ухудшить производительность своего кода. В частности, вы никогда не захотите писать реальный код, как показано выше. Для получения дополнительной информации о правильном (и неправильном) использовании Val, пожалуйста, прочтите более подробное обсуждение в разделе советов по производительности.
-
1«Маленький» определяется константой
MAX_UNION_SPLITTING, которая в настоящее время установлена в 4.
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.4.2/manual/types/