Типы
Системы типов традиционно делятся на два совершенно разных лагеря: статические системы типов, где каждый выражение программы должно иметь тип, вычислимый до выполнения программы, и динамические системы типов, где о типах ничего не известно до времени выполнения, когда доступны фактические значения, обрабатываемые программой. Объектно-ориентированное программирование позволяет некоторую гибкость в статически типизированных языках, позволяя писать код без знания точных типов значений во время компиляции. Способность писать код, который может работать с разными типами, называется полиморфизмом. Весь код в классических динамически типизированных языках является полиморфным: типы любых значений ограничиваются только при явном проверке типов или когда объекты не поддерживают операции во время выполнения.
Система типов 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 type. Общие синтаксисы для объявления абстрактного типа следующие:
abstract type «name» end abstract type «name» <: «supertype» end
Ключевое слово abstract type вводит новый абстрактный тип, имя которого задано «name». Это имя может быть необязательно послеведено <: и уже существующим типом, указывая, что только что объявленный абстрактный тип является подтипом этого «родительского» типа.
Если супертип не указан, по умолчанию супертипом является Any — предопределённый абстрактный тип, экземплярами которого являются все объекты, и все типы являются подтипами. В теории типов Any обычно называется «верхним» типом, так как он находится на вершине графа типов. В Julia также есть предопределённый абстрактный «нижний» тип, находящийся в нижней части графа типов, который записывается как Union{}. Он является точной противоположностью Any: ни один объект не является экземпляром Union{} и все типы являются супертипами Union{}.
Рассмотрим некоторые абстрактные типы, составляющие числовую иерархию Julia:
abstract type Number end abstract type Real <: Number end abstract type AbstractFloat <: Real end abstract type Integer <: Real end abstract type Signed <: Integer end abstract type Unsigned <: Integer end
Тип Number является прямым дочерним типом Any, а Real является его дочерним типом. В свою очередь, Real имеет двух потомков (их больше, но здесь показаны только два; мы рассмотрим другие позже): Integer и AbstractFloat, разделяя мир на представления целых чисел и представлений вещественных чисел. Представления вещественных чисел, конечно, включают типы с плавающей точкой, но также включают и другие типы, такие как рациональные числа. Следовательно, AbstractFloat является надлежащим подтипом Real, включающим только представления чисел с плавающей точкой. Целые числа далее подразделяются на Signed и Unsigned виды.
Оператор <: в общем случае означает "является подтипом", и, используемый в объявлениях такого рода, объявляет тип справа непосредственным супертипом нового объявленного типа. Он также может использоваться в выражениях как оператор подтипов, возвращающий true, когда его левый операнд является подтипом его правого операнда:
julia> Integer <: Number true julia> Integer <: AbstractFloat false
Важное применение абстрактных типов — предоставление по умолчанию реализаций для конкретных типов. Для примера рассмотрим:
function myplus(x,y)
x+y
end
Первое, что следует отметить, это то, что объявления аргументов выше эквивалентны x::Any и y::Any. Когда эта функция вызывается, например, как myplus(2,5), диспетчер выбирает наиболее специфический метод с именем myplus, который соответствует заданным аргументам. (См. Методы для получения дополнительной информации о множественном диспетчеризации.)
Предполагая, что не найден метод, более специфичный, чем приведенный выше, Julia затем неявно определяет и компилирует метод с именем myplus конкретно для двух Int аргументов на основе указанной выше обобщенной функции, т.е. неявно определяет и компилирует:
function myplus(x::Int,y::Int)
x+y
end
и, наконец, вызывает этот конкретный метод.
Таким образом, абстрактные типы позволяют программистам писать обобщенные функции, которые впоследствии могут использоваться в качестве метода по умолчанию многими комбинациями конкретных типов. Благодаря множественной диспетчеризации программист имеет полный контроль над тем, используется ли метод по умолчанию или более специфичный метод.
Важный момент, который следует отметить, заключается в том, что нет потери производительности, если программист полагается на функцию, аргументы которой являются абстрактными типами, потому что она перекомпилируется для каждой кортежа конкретных типов аргументов, с которыми она вызывается. (Однако может возникнуть проблема производительности в случае аргументов функций, являющихся контейнерами абстрактных типов; см. Рекомендации по производительности.)
Примитивные типы
Примитивный тип — это конкретный тип, данные которого состоят из обычных битов. Классическими примерами примитивных типов являются целые числа и значения с плавающей точкой. В отличие от большинства языков, Julia позволяет объявлять собственные примитивные типы, а не предоставлять только фиксированный набор встроенных. Фактически, все стандартные примитивные типы определены в самом языке:
primitive type Float16 <: AbstractFloat 16 end primitive type Float32 <: AbstractFloat 32 end primitive type Float64 <: AbstractFloat 64 end primitive type Bool <: Integer 8 end primitive type Char 32 end primitive type Int8 <: Signed 8 end primitive type UInt8 <: Unsigned 8 end primitive type Int16 <: Signed 16 end primitive type UInt16 <: Unsigned 16 end primitive type Int32 <: Signed 32 end primitive type UInt32 <: Unsigned 32 end primitive type Int64 <: Signed 64 end primitive type UInt64 <: Unsigned 64 end primitive type Int128 <: Signed 128 end primitive type UInt128 <: Unsigned 128 end
Общие синтаксисы для объявления примитивного типа:
primitive type «name» «bits» end primitive type «name» <: «supertype» «bits» end
Количество битов указывает, сколько памяти требуется для типа, а имя присваивает новое имя типу. Примитивный тип может быть необязательно объявлен как подтип некоторого супертипа. Если супертип опущен, то тип по умолчанию имеет Any в качестве своего непосредственного супертипа. Объявление Bool выше, следовательно, означает, что для хранения булева значения требуется восемь бит и Integer является его непосредственным супертипом. В настоящее время поддерживаются только размеры, кратные 8 битам. Поэтому булевы значения, хотя они действительно требуют всего одного бита, не могут быть объявлены меньше восьми битов.
Типы Bool, Int8 и UInt8 все имеют идентичное представление: они являются восьмибитными блоками памяти. Однако, поскольку система типов Julia номинирована, они не взаимозаменяемы, несмотря на идентичную структуру. Фундаментальное различие между ними заключается в том, что у них разные супертипы: непосредственным супертипом Bool является Integer, Int8 — Signed, а UInt8 — Unsigned. Все остальные различия между Bool, Int8 и UInt8 — это вопросы поведения — способы определения функций для работы, когда в качестве аргументов передаются объекты этих типов. Вот почему необходима номинированная система типов: если структура определяла тип, а тип, в свою очередь, определял поведение, то было бы невозможно заставить Bool вести себя иначе, чем Int8 или UInt8.
Составные типы
Составные типы называются записями, структурами или объектами в различных языках. Составной тип — это набор именованных полей, экземпляр которого может рассматриваться как одно значение. Во многих языках составные типы являются единственным видом определяемых пользователем типов, и они являются, пожалуй, наиболее часто используемым типом определяемым пользователем в Julia.
В распространенных языках объектно-ориентированного программирования, таких как C++, Java, Python и Ruby, составные типы также имеют связанные с ними именованные функции, и эта комбинация называется «объектом». В более чистых языках объектно-ориентированного программирования, таких как Ruby или Smalltalk, все значения являются объектами, независимо от того, являются ли они составными или нет. В менее чистых языках объектно-ориентированного программирования, включая C++ и Java, некоторые значения, такие как целые числа и значения с плавающей точкой, не являются объектами, в то время как экземпляры составных типов, определённых пользователем, являются истинными объектами с соответствующими методами. В Julia все значения являются объектами, но функции не объединены с объектами, на которых они работают. Это необходимо, поскольку Julia выбирает, какой метод функции использовать с помощью множественной диспетчеризации, что означает, что при выборе метода учитываются типы *всех* аргументов функции, а не только первого (см. Методы для получения дополнительной информации о методах и диспетчеризации). Таким образом, неуместно, чтобы функции «принадлежали» только своему первому аргументу. Организация методов в объекты функций, а не именованные наборы методов «внутри» каждого объекта, оказывается весьма полезным аспектом проектирования языка.
Составные типы вводятся с помощью ключевого слова struct, за которым следует блок имен полей, необязательно аннотированных типами с помощью оператора :::
julia> struct Foo
bar
baz::Int
qux::Float64
end
Поля без аннотации типа по умолчанию Any, и соответственно могут содержать любой тип значений.
Новые объекты типа Foo создаются путем применения объекта типа Foo как функции к значениям для его полей:
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)
julia> typeof(foo)
Foo
Когда тип применяется как функция, он называется конструктором. Автоматически генерируются два конструктора (они называются конструкторами по умолчанию). Один принимает любые аргументы и вызывает convert() для преобразования их в типы полей, а другой принимает аргументы, которые точно соответствуют типам полей. Причина, по которой оба генерируются, заключается в том, что это облегчает добавление новых определений, не заменяя непреднамеренно конструктор по умолчанию.
Так как поле bar не ограничено типом, подойдёт любое значение. Однако значение для baz должно быть преобразуемо в Int:
julia> Foo((), 23.5, 1)
ERROR: InexactError()
Stacktrace:
[1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
[2] 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
Составные объекты, объявленные с 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:
Объект с неизменяемым типом передаётся (как в операторах присваивания, так и в вызовах функций) путём копирования, тогда как изменяемый тип передаётся по ссылке.
Не разрешается изменять поля составного неизменяемого типа.
Показательно, особенно для читателей, имеющих опыт работы с 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: TypeError: 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.
Параметризованные составные типы
Параметры типа вводятся сразу после имени типа, заключённые в фигурные скобки:
julia> struct 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 сам по себе также является допустимым объектом типа, содержащим все экземпляры 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{T<:Real}(p::Point{T}) или function norm(p::Point{T} where T<:Real); см. Типы UnionAll.)
Более подробные примеры будут обсуждаться позднее в Методах.
Как создать объект 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.
Stacktrace:
[1] Point{Float64}(::Float64) at ./sysimg.jl:77
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} может использоваться для выражения аналога Julia ковариантного типа, тогда как 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: Pointy: in T, expected T<:Real, got Type{AbstractString}
julia> Pointy{1}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Int64
Параметры типа для параметрических составных типов могут быть ограничены аналогичным образом:
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
Чтобы привести реальный пример того, как вся эта параметрическая механика типов может быть полезна, вот фактическое определение неизменяемого типа Julia Rational (кроме того, что мы опускаем здесь конструктор для простоты), представляющего точное отношение целых чисел:
struct Rational{T<:Integer} <: Real
num::T
den::T
end
Имеет смысл брать отношения только целых значений, поэтому тип параметра T ограничен тем, что он является подтипом Integer, и отношение целых чисел представляет значение на числовой прямой, поэтому любой Rational является экземпляром абстракции Real.
Типы кортежей
Кортежи представляют собой абстракцию аргументов функции — без самой функции. Важные аспекты аргументов функции — это их порядок и типы. Поэтому тип кортежа похож на параметрический неизменяемый тип, где каждый параметр — тип одного поля. Например, тип кортежа из 2 элементов напоминает следующий неизменяемый тип:
struct Tuple2{A,B}
a::A
b::B
end
Однако есть три ключевых различия:
Типы кортежей могут иметь любое количество параметров.
Типы кортежей ковариантны по своим параметрам:
Tuple{Int}является подтипомTuple{Any}. Следовательно,Tuple{Any}считается абстрактным типом, а типы кортежей конкретны только если их параметры конкретны.Кортежи не имеют имен полей; к полям можно получить доступ только по индексу.
Значения кортежей записываются в скобках с запятыми. При построении кортежа соответствующий тип кортежа генерируется по требованию:
julia> typeof((1,"foo",2.5))
Tuple{Int64,String,Float64}
Обратите внимание на последствия ковариативности:
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true
julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false
julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false
Интуитивно это соответствует тому, что тип аргументов функции является подтипом сигнатуры функции (когда сигнатура совпадает).
Типы кортежей Vararg
Последний параметр типа кортежа может быть специальным типом Vararg, который обозначает любое количество последующих элементов:
julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString,Vararg{Int64,N} where N}
julia> isa(("1",), mytupletype)
true
julia> isa(("1",1), mytupletype)
true
julia> isa(("1",1,2), mytupletype)
true
julia> isa(("1",1,2,3.0), mytupletype)
false
Обратите внимание, что Vararg{T} соответствует нулю или более элементам типа T . Типы кортежей Vararg используются для представления аргументов, принимаемых методами Vararg (см. Функции Varargs).
Тип Vararg{T,N} соответствует ровно N элементам типа T . NTuple{N,T} — удобный псевдоним для Tuple{Vararg{T,N}}, т. е. тип кортежа, содержащий ровно N элемента типа T.
Типы Singleton
Здесь необходимо упомянуть особый вид абстрактного параметрического типа: типы singleton. Для каждого типа T «тип singleton» 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 — это просто абстрактный тип, у которого все типы объектов являются экземплярами, включая, конечно, типы singleton:
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
Пока мы не обсудим Параметрические методы и преобразования, сложно объяснить полезность конструкции типа singleton, но коротко: она позволяет специализировать поведение функции по определенным значениям типов. Это полезно для написания методов (особенно параметрических), поведение которых зависит от типа, заданного в качестве явного аргумента, а не подразумеваемого типом одного из его аргументов.
Несколько популярных языков имеют типы singleton, в том числе Haskell, Scala и Ruby. В общем случае термин «тип singleton» относится к типу, у которого единственный экземпляр — это одно значение. Это значение относится и к типам singleton Julia, но с оговоркой, что типы singleton существуют только у объектов-типов.
Параметрические примитивные типы
Примитивные типы также могут быть объявлены параметрически. Например, указатели представлены как примитивные типы, которые в 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. В отличие от целочисленных регистров, размеры регистров с плавающей точкой задаются стандартом IEEE-754. В то время как размер Int отражает размер системного указателя на этом компьютере.)
Операции над типами
Поскольку типы в Julia — это сами объекты, над ними можно выполнять обычные функции. Некоторые функции, которые особенно полезны для работы с типами или изучения типов, уже были представлены, например, оператор <:, который указывает, является ли левый операнд подтипом правого операнда.
Функция isa проверяет, является ли объект данного типа, и возвращает true или false:
julia> isa(1, Int) true julia> isa(1, AbstractFloat) false
Функция typeof(), уже используемая в примерах по всему руководству, возвращает тип своего аргумента. Поскольку, как отмечалось выше, типы являются объектами, у них тоже есть типы, и мы можем спросить, каковы их типы:
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,Float64,Rational})
DataType
julia> typeof(Union{Real,String})
Union
Что если мы повторим процесс? Каков тип типа типа? Как это ни странно, все типы — составные значения, и, следовательно, у всех них тип DataType:
julia> typeof(DataType) DataType julia> typeof(Union) DataType
DataType — это его собственный тип.
Другая операция, которая применяется к некоторым типам, — это supertype(), которая раскрывает надтип типа. Только объявленные типы (DataType) имеют однозначные надтипы:
julia> supertype(Float64) AbstractFloat julia> supertype(Number) Any julia> supertype(AbstractString) Any julia> supertype(Any) Any
Если применить supertype() к другим объектам типов (или не-типовым объектам), возникает MethodError:
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
supertype(!Matched::DataType) at operators.jl:41
supertype(!Matched::UnionAll) at operators.jl:46
Настройка красивого вывода
Часто необходимо настроить отображение экземпляров типа. Это достигается перегрузкой функции 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{T}(io::IO, ::MIME"text/plain", z::Polar{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)
где однострочный формат print() для массива значений Polar всё ещё используется. Технически, REPL вызывает display(z) для отображения результата выполнения строки, что по умолчанию эквивалентно show(STDOUT, MIME("text/plain"), z), которое, в свою очередь, по умолчанию эквивалентно show(STDOUT, z), но вы не должны определять новые методы display(), если не определяете новый обработчик мультимедийного вывода (см. Мультимедийный ввод/вывод).
Кроме того, вы можете определить методы show для других типов MIME, чтобы включить более богатые способы отображения (HTML, изображения и т. д.) объектов в средах, которые поддерживают это (например, IJulia). Например, мы можем определить отформатированное отображение HTML объектов Polar, с надстрочными и курсивными символами, с помощью:
julia> Base.show{T}(io::IO, ::MIME"text/html", z::Polar{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
"Типы значений"
В Julia невозможно выполнить диспетчеризацию по значению, такому как true или false. Однако вы можете выполнить диспетчеризацию по параметрическим типам, и Julia позволяет включать значения «простых битов» (Типы, Символы, Целые числа, числа с плавающей точкой, кортежи и т.д.) в качестве параметров типа. Распространённый пример — параметр размерности в Array{T,N}, где T — это тип (например, Float64), но N — просто Int.
Вы можете создавать собственные пользовательские типы, которые принимают значения в качестве параметров, и использовать их для управления диспетчеризацией пользовательских типов. В качестве иллюстрации этой идеи давайте введём параметрический тип Val{T}, который служит обычным способом использования этой техники в случаях, когда вам не нужна более сложная иерархия.
Val определяется как:
julia> struct Val{T}
end
Это всё, что нужно для реализации Val. Некоторые функции стандартной библиотеки Julia принимают типы Val в качестве аргументов, и вы также можете использовать их для написания собственных функций. Например:
julia> firstlast(::Type{Val{true}}) = "First"
firstlast (generic function with 1 method)
julia> firstlast(::Type{Val{false}}) = "Last"
firstlast (generic function with 2 methods)
julia> firstlast(Val{true})
"First"
julia> firstlast(Val{false})
"Last"
Для согласованности в Julia, вызов всегда должен передавать тип Valтипа, а не создавать экземпляр, т.е. использовать foo(Val{:bar}) вместо foo(Val{:bar}()).
Стоит отметить, что очень легко неправильно использовать параметрические типы «значений», включая Val; в неблагоприятных случаях вы легко можете значительно ухудшить производительность своего кода. В частности, вы никогда не захотите писать реальный код, как показано выше. Для получения дополнительной информации о правильном (и неправильном) использовании Val, обратитесь к более подробному обсуждению в разделе о советах по производительности.
Типы, допускающие значения NULL: Представление пропущенных значений
Во многих ситуациях вам нужно взаимодействовать со значением типа T, которое может или не может существовать. Для обработки таких ситуаций Julia предоставляет параметрический тип Nullable{T}, который можно рассматривать как специализированный контейнерный тип, который может содержать ноль или одно значение. Nullable{T} предоставляет минимальный интерфейс, предназначенный для обеспечения безопасности взаимодействия с пропущенными значениями. В настоящее время интерфейс состоит из нескольких возможных взаимодействий:
Создать объект
Nullable.Проверить, содержит ли объект
Nullableпропущенное значение.Получить значение объекта
Nullableс гарантией, что будет вызвано исключениеNullException, если значение объекта отсутствует.Получить значение объекта
Nullableс гарантией, что будет возвращено значение по умолчанию типаTв случае отсутствия значения объекта.Выполнить операцию над значением (если оно существует) объекта
Nullable, получив результатNullable. Результат будет пропущен, если исходное значение было пропущено.Выполнить проверку над значением (если оно существует) объекта
Nullableи получить результат, который будет пропущен, если сам объектNullableбыл пропущен или проверка не удалась.Выполнение общих операций над отдельными объектами
Nullable, распространяя пропущенные данные.
Создание объектов 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()
Stacktrace:
[1] get(::Nullable{Float64}) at ./nullable.jl:92
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
Убедитесь, что тип значения по умолчанию, переданного в get(), и тип объекта Nullable совпадают, чтобы избежать нестабильности типов, что может негативно сказаться на производительности. Используйте convert() вручную, если необходимо.
Выполнение операций с объектами Nullable
Объекты Nullable представляют значения, которые могут отсутствовать, и вся программа может быть написана с использованием этих объектов, сначала проверив отсутствие значения с помощью isnull(), а затем выполнив соответствующее действие. Однако существуют некоторые общие случаи использования, когда код может быть более лаконичным или понятным при использовании функций высшего порядка.
Функция map принимает в качестве аргументов функцию f и значение Nullable x. Она производит значение Nullable:
Если
xявляется отсутствующим значением, то она производит отсутствующее значение;Если
xимеет значение, то она производит значениеNullable, содержащееf(get(x))в качестве значения.
Это полезно для выполнения простых операций над значениями, которые могут отсутствовать, если желаемое поведение заключается в простом распространении отсутствующих значений.
Функция filter принимает в качестве аргументов функцию-предикат p (то есть функцию, возвращающую булево значение) и значение Nullable x. Она производит значение Nullable:
Если
xявляется отсутствующим значением, то она производит отсутствующее значение;Если
p(get(x))равно true, то она производит исходное значениеx;Если
p(get(x))равно false, то она производит отсутствующее значение.
Таким образом, filter можно рассматривать как выбор только допустимых значений и преобразование недопустимых значений в отсутствующие значения.
Хотя map и filter полезны в конкретных случаях, наиболее полезной функцией высшего порядка является broadcast, которая может обрабатывать широкий спектр случаев, в том числе, заставляя существующие операции работать и распространять Nullable. Пример продемонстрирует необходимость broadcast. Предположим, что у нас есть функция, которая вычисляет наибольший из двух действительных корней квадратного уравнения, используя формулу Виета:
julia> root(a::Real, b::Real, c::Real) = (-b + √(b^2 - 4a*c)) / 2a root (generic function with 1 method)
Мы можем проверить, что результат root(1, -9, 20) равен 5.0, как и ожидалось, так как 5.0 является наибольшим из двух действительных корней квадратного уравнения.
Предположим теперь, что мы хотим найти наибольший действительный корень квадратного уравнения, где коэффициенты могут быть отсутствующими значениями. Наличие отсутствующих значений в наборах данных является распространенным явлением в реальных данных, поэтому важно уметь с ними работать. Но мы не можем найти корни уравнения, если не знаем всех коэффициентов. Лучшее решение будет зависеть от конкретного случая; возможно, мы должны выбросить ошибку. Однако в этом примере мы будем считать, что лучшее решение заключается в распространении отсутствующих значений; то есть, если какой-либо вход отсутствует, мы просто генерируем отсутствующий выход.
Функция broadcast() упрощает эту задачу; мы можем просто передать функцию root , которую мы написали, в broadcast:
julia> broadcast(root, Nullable(1), Nullable(-9), Nullable(20))
Nullable{Float64}(5.0)
julia> broadcast(root, Nullable(1), Nullable{Int}(), Nullable{Int}())
Nullable{Float64}()
julia> broadcast(root, Nullable{Int}(), Nullable(-9), Nullable(20))
Nullable{Float64}()
Если один или несколько входов отсутствуют, то выход broadcast() будет отсутствовать.
Существует специальный синтаксический сахар для функции broadcast() с использованием нотации точек:
julia> root.(Nullable(1), Nullable(-9), Nullable(20))
Nullable{Float64}(5.0)
В частности, обычные арифметические операторы могут быть broadcast() удобно с использованием префиксов .:
julia> Nullable(2) ./ Nullable(3) .+ Nullable(1.0)
Nullable{Float64}(1.66667)
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/types/