Типы
Системы типов традиционно разделяются на два достаточно разных лагеря: статические системы типов, где каждый программный выражение должно иметь тип, вычислимый до выполнения программы, и динамические системы типов, где о типах ничего не известно до времени выполнения, когда доступны фактические значения, обрабатываемые программой. Объектно-ориентированность допускает некоторую гибкость в статически типизированных языках, позволяя писать код без точных типов значений, известных на этапе компиляции. Способность писать код, который может работать с разными типами, называется полиморфизмом. Весь код в классических динамически типизированных языках является полиморфным: только явно проверяя типы или когда объекты не поддерживают операции во время выполнения, типы любых значений когда-либо ограничиваются.
Система типов Julia динамическая, но приобретает некоторые преимущества статических систем типов, позволяя указывать, что некоторые значения имеют определенные типы. Это может быть очень полезно для генерации эффективного кода, но, что еще более важно, это позволяет диспатчу методов по типам аргументов функций глубоко интегрироваться в язык. Диспач методов подробно изучается в Методах, но укоренен в системе типов, представленной здесь.
По умолчанию в Julia, когда типы опущены, значения могут быть любого типа. Таким образом, можно написать много полезных программ Julia, никогда не используя типы явно. Однако, когда требуется дополнительная выразительность, легко постепенно вводить явные аннотации типов в ранее «бестиповый» код. Это обычно увеличивает производительность и надежность этих систем, и, возможно, несколько неожиданно, часто значительно упрощает их.
Описывая Julia на языке систем типов, это: динамическая, именованная и параметризованная. Обобщенные типы могут быть параметризованы, а иерархические отношения между типами явно объявляются, а не подразумеваются совместимой структурой. Одной из особенно отличительных особенностей системы типов Julia является то, что конкретные типы не могут быть подтипами друг друга: все конкретные типы являются окончательными и могут иметь только абстрактные типы в качестве своих надтипов. Хотя на первый взгляд это может показаться излишне ограничительно, оно имеет множество положительных последствий с удивительно небольшим количеством недостатков. Оказывается, что способность унаследовать поведение гораздо важнее, чем способность унаследовать структуру, и наследование обоих вызывает значительные трудности в традиционных объектно-ориентированных языках. Другие высокоуровневые аспекты системы типов Julia, которые следует упомянуть сразу, следующие:
- Нет разделения между объектами и значениями, не являющимися объектами: все значения в Julia являются истинными объектами, имеющими тип, принадлежащий одному полностью соединённому графу типов, все узлы которого равноправны как типы.
- Нет никакого осмысленного понятия «типа времени компиляции»: единственным типом значения является его фактический тип во время выполнения программы. Это называется «типом времени выполнения» в объектно-ориентированных языках, где сочетание статической компиляции с полиморфизмом делает это различие значительным.
- Типы имеют только значения, а не переменные — переменные просто имена, связанные со значениями.
- И абстрактные, и конкретные типы могут быть параметризованы другими типами. Они также могут параметризоваться символами, значениями любого типа, для которого
isbits()возвращает true (по существу, такие вещи, как числа и булевы значения, которые хранятся как типы C или структуры без указателей на другие объекты), а также кортежами из них. Параметры типа могут быть опущены, когда они не должны быть ссылаться или ограничивать.
Система типов Julia разработана для того, чтобы быть мощной и выразительной, но ясной, интуитивной и незаметной. Многие программисты Julia могут никогда не почувствовать необходимости писать код, который явно использует типы. Однако некоторые виды программирования становятся яснее, проще, быстрее и надежнее с объявленными типами.
Объявления типов
Оператор :: может использоваться для добавления аннотаций типов к выражениям и переменным в программах. Есть две основные причины для этого:
- В качестве утверждения для проверки того, что ваша программа работает так, как вы ожидаете,
- Для предоставления дополнительной информации о типах компилятору, который в некоторых случаях может улучшить производительность
Когда он прикреплён к выражению, вычисляющему значение, оператор :: читается как «является экземпляром». Его можно использовать где угодно, чтобы утверждать, что значение выражения слева является экземпляром типа справа. Когда тип справа конкретный, значение слева должно иметь этот тип как свою реализацию — вспомните, что все конкретные типы являются окончательными, поэтому никакая реализация не является подтипом другого. Когда тип является абстрактным, достаточно, чтобы значение реализовывалось конкретным типом, который является подтипом абстрактного типа. Если утверждение типа неверно, выбрасывается исключение, в противном случае возвращается значение левой части:
julia> (1+2)::AbstractFloat ERROR: TypeError: 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. Общие синтаксисы объявления абстрактного типа:
abstract «name» abstract «name» <: «supertype»
Ключевое слово abstract вводит новый абстрактный тип, имя которого задается «name». Это имя может быть дополнительно дополнено <: и уже существующим типом, указывая, что только что объявленный абстрактный тип является подтипом этого «родительского» типа.
Если надтип не указан, по умолчанию надтипом является Any — предопределенный абстрактный тип, экземплярами которого являются все объекты, и все типы являются его подтипами. В теории типов Any обычно называется «верхним» типом, так как он находится на вершине графа типов. Julia также имеет предопределенный абстрактный «нижний» тип, находящийся в нижней части графа типов, который записывается как Union{}. Он является полной противоположностью Any: ни один объект не является экземпляром Union{} и все типы являются надтипами Union{}.
Давайте рассмотрим некоторые из абстрактных типов, составляющих числовую иерархию Julia:
abstract Number abstract Real <: Number abstract AbstractFloat <: Real abstract Integer <: Real abstract Signed <: Integer abstract Unsigned <: Integer
Тип 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 позволяет объявлять свои собственные типы бит вместо того, чтобы предоставлять только фиксированный набор встроенных типов бит. Фактически, стандартные типы бит определяются самим языком:
bitstype 16 Float16 <: AbstractFloat bitstype 32 Float32 <: AbstractFloat bitstype 64 Float64 <: AbstractFloat bitstype 8 Bool <: Integer bitstype 32 Char bitstype 8 Int8 <: Signed bitstype 8 UInt8 <: Unsigned bitstype 16 Int16 <: Signed bitstype 16 UInt16 <: Unsigned bitstype 32 Int32 <: Signed bitstype 32 UInt32 <: Unsigned bitstype 64 Int64 <: Signed bitstype 64 UInt64 <: Unsigned bitstype 128 Int128 <: Signed bitstype 128 UInt128 <: Unsigned
Общие синтаксисы для объявления bitstype:
bitstype «bits» «name» bitstype «bits» «name» <: «supertype»
Количество бит указывает, сколько памяти требуется для типа, а имя дает новому типу имя. Тип бит может быть необязательно объявлен как подтип какого-либо супертипа. Если супертип опущен, тип по умолчанию имеет Any в качестве непосредственного супертипа. Объявление Bool выше, следовательно, означает, что для хранения булева значения требуется восемь бит, а его непосредственный супертип — Integer. В настоящее время поддерживаются только размеры, кратные 8 битам. Следовательно, булевы значения, хотя они действительно требуют всего одного бита, не могут быть объявлены меньше, чем восемь бит.
Типы Bool, Int8 и UInt8 все имеют одинаковое представление: это восьмибитные части памяти. Однако, поскольку система типов Julia номиналистическая, они не взаимозаменяемы, несмотря на идентичную структуру. Другое фундаментальное различие заключается в том, что у них разные супертипы: непосредственный супертип Bool — это Integer, Int8 — Signed, а UInt8 — Unsigned. Все остальные различия между Bool, Int8, и UInt8 относятся к поведению — способу определения функций для работы с объектами этих типов в качестве аргументов. Именно поэтому необходима номиналистическая система типов: если структура определяла тип, который, в свою очередь, диктует поведение, то было бы невозможно заставить Bool вести себя по-другому, чем Int8 или UInt8.
Составные типы
Составные типы называются записями, структурами (struct в C) или объектами на различных языках. Составной тип — это набор именованных полей, экземпляр которых может рассматриваться как одно значение. Во многих языках составные типы являются единственным типом определяемого пользователем типа, и они, безусловно, являются наиболее часто используемым определяемым пользователем типом в Julia.
В распространенных объектно-ориентированных языках, таких как C++, Java, Python и Ruby, составные типы также имеют связанные с ними именованные функции, и это сочетание называется «объектом». В более чистых объектно-ориентированных языках, таких как Ruby или Smalltalk, все значения являются объектами, независимо от того, являются ли они составными или нет. В менее чистых объектно-ориентированных языках, включая C++ и Java, некоторые значения, такие как целые числа и числа с плавающей запятой, не являются объектами, а экземпляры определяемых пользователем составных типов являются истинными объектами с ассоциированными методами. В Julia все значения являются объектами, но функции не объединяются с объектами, над которыми они работают. Это необходимо, поскольку Julia выбирает, какой метод функции использовать, с помощью множественного диспетчеризации, что означает, что типы *всех* аргументов функции учитываются при выборе метода, а не только первого (см. Методы для получения дополнительной информации о методах и диспетчеризации). Таким образом, было бы неуместно, чтобы функции «принадлежали» только своему первому аргументу. Организация методов в функциях, а не именованные пакеты методов «внутри» каждого объекта, в конечном итоге становится очень полезным аспектом проектирования языка.
Поскольку составные типы являются наиболее распространенной формой определяемых пользователем конкретных типов, они просто вводятся с помощью ключевого слова type и последующего блока имен полей, необязательно аннотированных типами с помощью оператора :::
julia> type 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()
in Foo(::Tuple{}, ::Float64, ::Int64) at ./none:2
...
Список имен полей можно получить, используя функцию fieldnames.
julia> fieldnames(foo)
3-element Array{Symbol,1}:
:bar
:baz
:qux
Значения полей составного объекта можно получить с помощью традиционной записи foo.bar:
julia> foo.bar "Hello, world." julia> foo.baz 23 julia> foo.qux 1.5
Значения также можно изменять, как ожидается:
julia> foo.qux = 2 2 julia> foo.bar = 1//2 1//2
Составные типы без полей являются одиночными; может существовать только один экземпляр таких типов:
type NoFields end julia> is(NoFields(), NoFields()) true
Функция is подтверждает, что два созданных экземпляра NoFields на самом деле являются одним и тем же. Одиночные типы подробно описаны ниже ниже.
Есть многое, что можно сказать о том, как создаются экземпляры составных типов, но этот вопрос зависит как от параметрических типов, так и от методов и достаточно важен, чтобы быть рассмотренным в отдельном разделе: Конструкторы.
Неизменяемые составные типы
Также можно определить *неизменяемые* составные типы, используя ключевое слово immutable вместо type:
immutable Complex
real::Float64
imag::Float64
end
Поведение таких типов во многом аналогично другим составным типам, за исключением того, что экземпляры их не могут быть изменены. Неизменяемые типы обладают рядом преимуществ:
- В некоторых случаях они более эффективны. Типы, такие как пример
Complexвыше, могут быть эффективно упакованы в массивы, и в некоторых случаях компилятор может избежать выделения неизменяемых объектов вообще. - Невозможно нарушить инварианты, предоставляемые конструкторами типа.
- Код, использующий неизменяемые объекты, может быть проще для понимания.
Неизменяемый объект может содержать изменяемые объекты, такие как массивы, в качестве полей. Эти содержащиеся объекты останутся изменяемыми; только поля самого неизменяемого объекта нельзя изменить на указание на другие объекты.
Полезно рассматривать неизменяемые составные части как связанные с определенными значениями полей — только значения полей полностью определяют объект. В отличие от этого, изменяемый объект похож на небольшой контейнер, который может содержать разные значения со временем, и поэтому не идентифицируется с конкретными значениями полей. При принятии решения о том, следует ли сделать тип неизменяемым, спросите себя, будут ли два экземпляра с одинаковыми значениями полей считаться идентичными или им может потребоваться независимое изменение со временем. Если они будут считаться идентичными, тип, вероятно, должен быть неизменяемым.
В заключение, два основных свойства определяют неизменяемость в Julia:
- Объект с неизменяемым типом передается (как в операциях присваивания, так и в вызовах функций) путем копирования, в то время как изменяемый тип передается по ссылке.
- Изменение полей составного неизменяемого типа запрещено.
Полезно, особенно для читателей, знакомых с C/C++, понять, почему эти два свойства идут рука об руку. Если бы они были разделены, т. е. если бы поля объектов, передаваемых по копированию, могли быть изменены, то стало бы труднее рассуждать о некоторых экземплярах обобщенного кода. Например, предположим, что x — это аргумент функции абстрактного типа, и предположим, что функция изменяет поле: x.isprocessed = true. В зависимости от того, передается ли x путем копирования или по ссылке, данное утверждение может или не может изменить фактический аргумент в вызывающей программе. Julia обходит возможность создания функций с неизвестными эффектами в этой ситуации, запрещая изменение полей объектов, передаваемых путем копирования.
Объявленные типы
Три типа, обсуждавшиеся в предыдущих трех разделах, на самом деле тесно связаны. У них есть общие ключевые свойства:
- Они явно объявлены.
- У них есть имена.
- У них есть явно объявленные супертипы.
- У них могут быть параметры.
Из-за этих общих свойств эти типы представлены внутри как экземпляры одной и той же концепции, DataType, которая является типом любого из этих типов:
julia> typeof(Real) DataType julia> typeof(Int) DataType
Тип DataType может быть абстрактным или конкретным. Если он конкретный, у него есть заданный размер, расположение в памяти и (необязательно) имена полей. Таким образом, тип bits является DataType с ненулевым размером, но без имён полей. Композитный тип — это DataType, имеющий имена полей или пустой (нулевого размера).
Каждое конкретное значение в системе является экземпляром некоторого DataType.
Объединения типов
Объединение типов — это специальный абстрактный тип, включающий в себя все экземпляры любого из его аргументных типов, созданный с использованием специальной функции Union:
julia> IntOrString = Union{Int,AbstractString}
Union{AbstractString,Int64}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: type: typeassert: expected Union{AbstractString,Int64}, got Float64
Компиляторы многих языков имеют внутреннюю конструкцию объединения для рассуждений о типах; Julia просто предоставляет её программисту.
Параметрические типы
Важной и мощной особенностью системы типов Julia является её параметричность: типы могут принимать параметры, так что объявления типов фактически вводят целое семейство новых типов — по одному для каждой возможной комбинации значений параметров. Существует много языков, поддерживающих ту или иную форму обобщённого программирования, в котором структуры данных и алгоритмы для их обработки могут быть определены без указания точных типов. Например, некоторая форма обобщённого программирования существует в ML, Haskell, Ada, Eiffel, C++, Java, C#, F# и Scala, чтобы назвать лишь некоторые. Некоторые из этих языков поддерживают истинную параметрическую полиморфию (например, ML, Haskell, Scala), в то время как другие поддерживают неявную, основанную на шаблонах модель обобщённого программирования (например, C++, Java). С таким множеством различных видов обобщённого программирования и параметрических типов в различных языках мы даже не будем пытаться сравнить параметрические типы Julia с другими языками, а вместо этого сосредоточимся на объяснении системы Julia как таковой. Однако мы отметим, что поскольку Julia — язык динамической типизации и не нуждается в принятии всех решений о типах во время компиляции, многие традиционные трудности, встречающиеся в статических параметрических системах типов, могут быть относительно легко решены.
Все объявленные типы (изменчивого DataType вида) могут быть параметризованы с одинаковым синтаксисом в каждом случае. Мы обсудим их в следующем порядке: сначала параметрические композитные типы, затем параметрические абстрактные типы и, наконец, параметрические типы bits.
Параметрические композитные типы
Параметрические параметры вводятся непосредственно после имени типа, заключённые в фигурные скобки:
type Point{T}
x::T
y::T
end
Это объявление определяет новый параметрический тип Point{T}, содержащий две «координаты» типа T Что, можно спросить, такое T? Ну, в этом как раз и заключается суть параметрических типов: это может быть любой тип вообще (или значение любого типа bits, на самом деле, хотя здесь он явно используется как тип). 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 также является допустимым объектом типа:
julia> Point
Point{T}
Здесь T — это псевдоним типа, используемый в первоначальном объявлении Point. Что означает 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{T<:Real}(p::Point{T})
sqrt(p.x^2 + p.y^2)
end
Более подробные примеры будут рассмотрены позже в разделе Методы.
Как создать объект 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}
This may have arisen from a call to the constructor Point{Float64}(...),
since type constructors fall back to convert methods.
in Point{Float64}(::Float64) at ./sysimg.jl:53
...
julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
Closest candidates are:
Point{Float64}{T}(::Any, ::Any) at none:3
Point{Float64}{T}(::Any) at sysimg.jl:53
...
Для параметрических типов генерируется только один конструктор по умолчанию, так как его переопределение невозможно. Этот конструктор принимает любые аргументы и преобразует их к типам полей.
Во многих случаях излишне указывать тип объекта 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{T}(::Int64, ::Float64)
...
Методы конструкторов для надлежащего обработки таких смешанных случаев могут быть определены, но это не будет обсуждаться до тех пор, пока не будут рассмотрены в разделе Конструкторы позднее.
Параметрические абстрактные типы
Объявления параметрических абстрактных типов объявляют набор абстрактных типов аналогичным образом:
abstract Pointy{T}
В этом объявлении 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
Так же, как обычные абстрактные типы служат для создания полезной иерархии типов над конкретными типами, параметрические абстрактные типы служат той же цели в отношении параметрических составных типов. Мы могли бы, например, объявить Point{T} подтипом Pointy{T} следующим образом:
type 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
Какова цель параметрических абстрактных типов, таких как Pointy? Представьте, что мы создаём реализацию точки, которая требует только одной координаты, потому что точка находится на диагональной прямой x = y:
type DiagPoint{T} <: Pointy{T}
x::T
end
Теперь и Point{Float64}, и DiagPoint{Float64} являются реализациями абстракции Pointy{Float64}, и аналогично для каждого другого возможного выбора типа T. Это позволяет программировать на общей интерфейсной реализации всех объектов Pointy, реализованной как для Point, так и для DiagPoint. Однако это нельзя полностью продемонстрировать до тех пор, пока мы не введём методы и диспетчеризацию в следующем разделе Методы.
Существуют ситуации, когда параметры типа не должны свободно охватывать все возможные типы. В таких ситуациях можно ограничить диапазон T следующим образом:
abstract Pointy{T<:Real}
При таком объявлении допустимо использовать любой тип, который является подтипом Real вместо T, но не типы, которые не являются подтипами Real:
julia> Pointy{Float64}
Pointy{Float64}
julia> Pointy{Real}
Pointy{Real}
julia> Pointy{AbstractString}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Type{AbstractString}
...
julia> Pointy{1}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Int64
...
Параметры типа для параметрических составных типов могут быть ограничены аналогичным образом:
type Point{T<:Real} <: Pointy{T}
x::T
y::T
end
Чтобы продемонстрировать на реальном примере, как все это механизмы параметрических типов могут быть полезны, вот фактическое определение неизменяемого типа Джулии Rational (кроме того, что мы опускаем конструктор для простоты), представляющего точное отношение целых чисел:
immutable Rational{T<:Integer} <: Real
num::T
den::T
end
Принимать отношения только целочисленных значений, поэтому тип параметра T ограничен тем, что он является подтипом Integer, а отношение целых чисел представляет значение на числовой прямой, поэтому любой Rational является экземпляром абстракции Real.
Типы кортежей
Кортежи — это абстракция аргументов функции — без самой функции. Существенные аспекты аргументов функции — это их порядок и их типы. Поэтому тип кортежа похож на параметризованный неизменяемый тип, где каждый параметр является типом одного поля. Например, тип кортежа из 2 элементов похож на следующий неизменяемый тип:
immutable 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> isa(("1",), Tuple{AbstractString,Vararg{Int}})
true
julia> isa(("1",1), Tuple{AbstractString,Vararg{Int}})
true
julia> isa(("1",1,2), Tuple{AbstractString,Vararg{Int}})
true
julia> isa(("1",1,2,3.0), Tuple{AbstractString,Vararg{Int}})
false
Обратите внимание, что Vararg{T} соответствует нулю или более элементам типа T. Типы кортежей с произвольным числом аргументов используются для представления аргументов, принимаемых методами с произвольным числом аргументов (см. Функции с произвольным числом аргументов).
Тип Vararg{T,N} соответствует ровно N элементам типа T. NTuple{N,T} — это удобный псевдоним для Tuple{Vararg{T,N}}, т. е. типа кортежа, содержащего ровно N элементов типа T.
Типы-синглтоны
Здесь необходимо упомянуть особый вид абстрактного параметрического типа: типы-синглтоны. Для каждого типа 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:
bitstype 32 Ptr{T}
# 64-bit system:
bitstype 64 Ptr{T}
Несколько необычной особенностью этих объявлений по сравнению с типичными параметризованными составными типами является то, что параметр типа T не используется в определении самого типа — это просто абстрактная метка, по существу определяющая целую семью типов с одинаковой структурой, отличающихся только параметром типа. Таким образом, Ptr{Float64} и Ptr{Int64} являются различными типами, даже если они имеют одинаковое представление. И, конечно же, все специфические типы указателей являются подтипами обобщенного типа Ptr:
julia> Ptr{Float64} <: Ptr
true
julia> Ptr{Int64} <: Ptr
true
Псевдонимы типов
Иногда удобно ввести новое имя для уже выразимого типа. Для таких случаев Джулия предоставляет механизм typealias. Например, UInt является псевдонимом типа либо UInt32 или UInt64 в зависимости от размера указателей в системе:
# 32-bit system: julia> UInt UInt32 # 64-bit system: julia> UInt UInt64
Это достигается с помощью следующего кода в base/boot.jl:
if is(Int,Int64)
typealias UInt UInt64
else
typealias UInt UInt32
end
Конечно, это зависит от того, чему Int присваивается псевдоним — но это предварительно определено как правильный тип — либо Int32 или Int64.
Для параметризованных типов typealias может быть удобно указывать имена в тех случаях, когда некоторые параметры выбора фиксированы. Массивы Джулии имеют тип Array{T,N}, где T — тип элемента, а N — число измерений массива. Для удобства запись Array{Float64} позволяет указать тип элемента без указания измерения:
julia> Array{Float64,1} <: Array{Float64} <: Array
true
Однако нет простого способа ограничить только размерность, но не тип элемента. Тем не менее, часто необходимо гарантировать, что объект является вектором или матрицей (накладывая ограничения на число измерений). По этой причине предоставлены следующие псевдонимы типов:
typealias Vector{T} Array{T,1}
typealias Matrix{T} Array{T,2}
Запись Vector{Float64} эквивалентна записи Array{Float64,1}, и обобщенный тип Vector имеет в качестве экземпляров все объекты Array, где второй параметр — число измерений массива — равен 1, независимо от типа элемента. В языках, где параметризованные типы всегда должны быть указаны полностью, это не особенно полезно, но в Джулии это позволяет писать просто Matrix для абстрактного типа, включающего все двумерные плотные массивы любого типа элементов.
Это объявление Vector создает отношение подтипов Vector{Int} <: Vector. Однако не всегда верно, что параметрическое утверждение typealias создает такое отношение; например, утверждение:
typealias AA{T} Array{Array{T,1},1}
не создаёт отношения AA{Int} <: AA. Причина в том, что Array{Array{T,1},1} — это вообще не абстрактный тип; фактически, это конкретный тип, описывающий одномерный массив, в котором каждый элемент является объектом типа Array{T,1} для некоторого значения T.
Операции над типами
Поскольку типы в Джулии сами по себе являются объектами, обычные функции могут работать с ними. Некоторые функции, которые особенно полезны для работы с типами или исследования типов, уже были введены, например, оператор <:, который указывает, является ли его левый операнд подтипом его правого операнда.
Функция isa проверяет, является ли объект заданного типа, и возвращает true или false:
julia> isa(1,Int) true julia> isa(1,AbstractFloat) false
Функция typeof(), уже используемая во всем руководстве в примерах, возвращает тип своего аргумента. Поскольку, как отмечалось выше, типы являются объектами, у них тоже есть типы, и мы можем узнать, какими они являются:
julia> typeof(Rational)
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: `supertype` has no method matching supertype(::Type{Union{Float64,Int64}})
“Типы значений”
В Джулии вы не можете использовать диспетчер по значению, например, по true или false. Однако вы можете использовать диспетчер по параметризованным типам, и Джулия позволяет включать «простые битовые» значения (типы, символы, целые числа, числа с плавающей точкой, кортежи и т. д.) в качестве параметров типа. Типичный пример — параметр размерности в Array{T,N}, где T — тип (например, Float64), но N — просто Int.
Вы можете создавать свои собственные пользовательские типы, которые принимают значения в качестве параметров, и использовать их для управления диспетчеризацией пользовательских типов. В качестве иллюстрации этой идеи давайте введем параметризованный тип Val{T}, который служит обычным способом использования этой техники для случаев, когда вам не нужен более сложный иерархический подход.
Val определен как:
immutable Val{T}
end
Более нет в реализации Val кроме этого. Некоторые функции стандартной библиотеки Джулии принимают типы Val в качестве аргументов, и вы также можете использовать их для написания своих собственных функций. Например:
firstlast(::Type{Val{true}}) = "First"
firstlast(::Type{Val{false}}) = "Last"
julia> firstlast(Val{true})
"First"
julia> firstlast(Val{false})
"Last"
Для согласованности в Джулии место вызова всегда должно передавать тип Val , а не создавать экземпляр, т. е. использовать foo(Val{:bar}), а не foo(Val{:bar}()).
Стоит отметить, что очень легко неправильно использовать параметрические типы «значение», включая Val; в неблагоприятных случаях вы можете легко ухудшить производительность своего кода. В частности, вы никогда не должны писать фактический код, как показано выше. Для получения более подробной информации о правильном (и неправильном) использовании Val, пожалуйста, ознакомьтесь с более подробным обсуждением в подсказках по производительности.
Типы с возможностью отсутствия значения: представление пропущенных значений
Во многих ситуациях вам необходимо взаимодействовать со значением типа T, которое может или не может существовать. Для работы с такими ситуациями Julia предоставляет параметрический тип, называемый Nullable{T}, который можно рассматривать как специализированный контейнерный тип, который может содержать ноль или одно значение. Nullable{T} предоставляет минимальный интерфейс, разработанный для обеспечения безопасности взаимодействия с пропущенными значениями. В настоящее время интерфейс состоит из четырех возможных взаимодействий:
- Создайте объект
Nullable. - Проверьте, имеет ли объект
Nullableпропущенное значение. - Получите значение объекта
Nullableс гарантией, что будет выброшено исключениеNullException, если значение объекта отсутствует. - Получите значение объекта
Nullableс гарантией, что будет возвращено значение по умолчанию типаT, если значение объекта отсутствует.
Создание объектов Nullable
Для создания объекта, представляющего пропущенное значение типа T, используйте функцию Nullable{T}():
julia> x1 = Nullable{Int64}()
Nullable{Int64}()
julia> x2 = Nullable{Float64}()
Nullable{Float64}()
julia> x3 = Nullable{Vector{Int64}}()
Nullable{Array{Int64,1}}()
Для создания объекта, представляющего не пропущенное значение типа T, используйте функцию Nullable(x::T):
julia> x1 = Nullable(1)
Nullable{Int64}(1)
julia> x2 = Nullable(1.0)
Nullable{Float64}(1.0)
julia> x3 = Nullable([1, 2, 3])
Nullable{Array{Int64,1}}([1,2,3])
Обратите внимание на основное различие между этими двумя способами создания объекта Nullable: в одном стиле вы предоставляете тип T в качестве параметра функции; в другом стиле вы предоставляете единственное значение типа T в качестве аргумента.
Проверка наличия значения в объекте Nullable
Вы можете проверить наличие значения в объекте Nullable, используя isnull():
julia> isnull(Nullable{Float64}())
true
julia> isnull(Nullable(0.0))
false
Безопасный доступ к значению объекта Nullable
Вы можете безопасно получить доступ к значению объекта Nullable с помощью get():
julia> get(Nullable{Float64}())
ERROR: NullException()
in get(::Nullable{Float64}) at ./nullable.jl:62
...
julia> get(Nullable(1.0))
1.0
Если значение отсутствует, как это будет для Nullable{Float64}, будет выброшено исключение NullException. Исключение ошибки в функции get() гарантирует, что любая попытка получить доступ к отсутствующему значению немедленно завершится ошибкой.
В тех случаях, когда существует разумное значение по умолчанию, которое можно использовать, если значение объекта Nullable отсутствует, вы можете предоставить это значение по умолчанию как второй аргумент функции get():
julia> get(Nullable{Float64}(), 0.0)
0.0
julia> get(Nullable(1.0), 0.0)
1.0
Обратите внимание, что это значение по умолчанию автоматически преобразуется в тип объекта Nullable, к которому вы пытаетесь получить доступ с помощью функции get(). Например, в показанном выше коде значение 0 будет автоматически преобразовано в значение Float64 перед возвратом. Наличие значений по умолчанию упрощает использование функции get() для написания типабезопасного кода, который взаимодействует с источниками потенциально отсутствующих значений.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/types/