Типы
Системы типов традиционно делятся на два довольно разных лагеря: статические системы типов, где каждый выражение программы должно иметь тип, вычислимый до выполнения программы, и динамические системы типов, где ничего не известно о типах до времени выполнения, когда доступны фактические значения, обрабатываемые программой. Объектно-ориентированный подход допускает некоторую гибкость в статически типизированных языках, позволяя писать код без точных типов значений, известных на этапе компиляции. Способность писать код, который может работать с различными типами, называется полиморфизмом. Весь код в классических динамически типизированных языках является полиморфным: только явным проверкой типов или при сбое объектов при работе с операциями во время выполнения ограничиваются типы каких-либо значений.
Система типов 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
и, наконец, вызывает этот конкретный метод.
Таким образом, абстрактные типы позволяют программистам писать обобщенные функции, которые впоследствии могут использоваться в качестве метода по умолчанию для многих комбинаций конкретных типов. Благодаря множественному диспетчеру программист имеет полный контроль над тем, используется ли метод по умолчанию или более конкретный метод.
Важно отметить, что нет потери производительности, если программист полагается на функцию, аргументы которой являются абстрактными типами, потому что она перекомпилируется для каждой кортежа конкретных типов аргументов, с которыми она вызывается. (Однако может возникнуть проблема производительности в случае аргументов функций, которые являются контейнерами абстрактных типов; см. Рекомендации по производительности.)
Primitive Types
Примитивный тип — это конкретный тип, данные которого состоят из обычных битов. Классические примеры примитивных типов — целые числа и значения с плавающей точкой. В отличие от большинства языков, 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.
Composite Types
Составные типы называются записями, структурами или объектами в разных языках. Составной тип — это коллекция именованных полей, экземпляр которого может рассматриваться как одно значение. Во многих языках составные типы — единственный тип, который можно определить пользователем, а они также являются наиболее часто используемыми типами, определяемыми пользователем, в 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(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 Composite Types
Если составной тип объявлен с 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; см. Объединения универсальных типов.)
Дополнительные примеры будут обсуждаться позднее в разделе Методы.
Как создать объект 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: Cannot `convert` an object of type Float64 to an object of type Point{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 и т. д.). Как это работает? 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 определённого размера. В отличие от целых регистров, размеры регистров с плавающей точкой определяются стандартом IEEE-754. В то время как размер Int отражает размер родного указателя на данной машине.)
Операции над типами
Поскольку типы в Julia — это сами объекты, над ними могут выполняться обычные функции. Некоторые функции, которые особенно полезны при работе с типами или исследовании типов, уже были представлены, такие как оператор <:, который указывает, является ли его левый операнд подтипом его правого операнда.
Функция isa проверяет, является ли объект данного типа, и возвращает true или false:
julia> isa(1, Int) true julia> isa(1, AbstractFloat) false
Функция typeof, уже используемая на протяжении всей справки в примерах, возвращает тип своего аргумента. Поскольку, как отмечалось выше, типы являются объектами, они также имеют типы, и мы можем спросить, какими являются их типы:
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,Float64,Rational})
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, который позволяет передавать контекстные свойства вместе с обернутым потоком IO. Например, мы можем создать более короткую форму представления в нашем методе 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
Это новое компактное представление будет использоваться, когда переданный поток IO является объектом 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, пожалуйста, прочтите более подробное обсуждение в разделе советов по производительности.
«Маленькое» определяется константой MAX_UNION_SPLITTING, которая в настоящее время установлена в 4.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v0.7.0/manual/types/