Типы
Системы типов традиционно делятся на два принципиально разных лагеря: статические системы типов, где каждый выражение программы должно иметь тип, вычисляемый до выполнения программы, и динамические системы типов, где о типах ничего не известно до времени выполнения, когда доступны фактические значения, обрабатываемые программой. Объектно-ориентированный подход допускает определенную гибкость в статически типизированных языках, позволяя писать код без точного знания типов значений во время компиляции. Способность писать код, который может работать с разными типами, называется полиморфизмом. Весь код в классических динамически типизированных языках является полиморфным: типы любых значений ограничиваются только при явном проверке типов или при неудачных операциях с объектами во время выполнения.
Система типов 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 a value of type 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 запустить стандартные примитивные типы, которые поддерживает LLVM. После их определения, практически нет причин определять больше.
Примитивный тип — это конкретный тип, данные которого состоят из обычных битов. Классическими примерами примитивных типов являются целые числа и значения с плавающей запятой. В отличие от большинства языков, 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 битам, и вы, вероятно, столкнетесь с ошибками LLVM при использовании размеров, отличных от указанных выше. Поэтому булевы значения, хотя и требуют всего одного бита, не могут быть объявлены меньше восьми бит.
Типы 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
Тип 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 a value of type Float64
Компиляторы многих языков имеют внутреннюю конструкцию объединений для анализа типов; Julia просто предоставляет её программисту. Компилятор Julia способен генерировать эффективный код при наличии типов Union с небольшим числом типов [1], генерируя специализированный код в отдельных ветвях для каждого возможного типа.
Особо полезный случай типа Union — это Union{T, Nothing}, где T может быть любым типом, а Nothing — это единственный тип, единственный экземпляр которого — объект nothing. Эта конструкция является аналогом 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} — это «точка», «координаты» которой — строковые объекты (см. Strings).
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 a value of type Int64
Параметры типа параметризованных составных типов можно ограничить таким же образом:
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
Чтобы привести реальный пример того, как вся эта параметрическая механизма типов может быть полезна, вот фактическое определение неизменяемого типа Rational языка Julia (кроме конструктора для простоты), представляющего точную дробь целых чисел:
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, который обозначает любое число последующих элементов:
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{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 предоставляет более удобный синтаксис, подобный struct, для объявления типов NamedTuple через объявления key::Type, где пропущенный ::Type соответствует ::Any.
julia> @NamedTuple{a::Int, b::String}
NamedTuple{(:a, :b),Tuple{Int64,String}}
julia> @NamedTuple begin
a::Int
b::String
end
NamedTuple{(:a, :b),Tuple{Int64,String}}
Тип NamedTuple может использоваться в качестве конструктора, принимающего один кортежный аргумент. Построенный тип NamedTuple может быть либо конкретным типом с указанными параметрами, либо типом, который задаёт только имена полей:
julia> @NamedTuple{a::Float32,b::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. В общем случае термин «тип-экземпляр» относится к типу, единственным экземпляром которого является одно значение. Это значение относится к типам-экземплярам Julia, но с оговоркой, что типы-экземпляры существуют только у объектов типов.
Параметризованные примитивные типы
Примитивные типы также могут быть объявлены параметризованными. Например, указатели представлены как примитивные типы, которые объявляются в Julia следующим образом:
# 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:
[...]
Настройка красивого вывода
Часто требуется настроить отображение экземпляров типа. Это достигается перегрузкой функции 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) с 2 аргументами.) Это приводит к:
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 в одну строку должен выводить корректное выражение Джулии для создания показанного объекта. Когда этот 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 для списка общих свойств, которые можно использовать для настройки вывода.
"Типы значений"
В Джулии нельзя диспатчить по значению, такому как true или false. Однако вы можете диспатчить по параметрическим типам, и Джулия позволяет включать значения «чистых битов» (Типы, Символы, Целые числа, числа с плавающей точкой, кортежи и т. д.) в качестве параметров типа. Типичным примером является параметр размерности в 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 не содержит ничего большего. Некоторые функции стандартной библиотеки Джулии принимают экземпляры 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"
Для согласованности в Джулии в месте вызова всегда следует передавать экземпляр 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.5.3/manual/types/