Spec-Zone.ru › Julia 1.2

Типы

Системы типов традиционно делятся на два совершенно разных лагеря: статические системы типов, где каждый выражение программы должно иметь тип, вычислимый до выполнения программы, и динамические системы типов, где ничего не известно о типах до времени выполнения, когда доступны фактические значения, используемые программой. Объектно-ориентированное программирование допускает некоторую гибкость в статически типизированных языках, позволяя писать код без точного знания типов значений во время компиляции. Способность писать код, который может работать с различными типами, называется полиморфизмом. Весь код в классических динамически типизированных языках является полиморфным: только явно проверяя типы или когда объекты не поддерживают операции во время выполнения, типы каких-либо значений когда-либо ограничиваются.

Система типов Julia динамическая, но приобретает некоторые преимущества статических систем типов, позволяя указывать, что определённые значения являются определённого типа. Это может быть очень полезно для генерации эффективного кода, но ещё более существенно, это позволяет глубоко интегрировать в язык диспетчеризацию методов по типам аргументов функций. Диспетчеризация методов подробно изучается в Методах, но укоренена в системе типов, представленной здесь.

По умолчанию в Julia, когда типы опущены, значения могут быть любого типа. Таким образом, можно написать много полезных функций Julia, никогда не используя явно типы. Однако, когда требуется дополнительная выразительность, легко постепенно вводить явные аннотации типов в ранее «нетипизированный» код. Добавление аннотаций служит трём основным целям: использовать мощный механизм множественной диспетчеризации Julia, повысить читаемость для человека и обнаруживать ошибки программиста.

Описывая Julia языком систем типов, это: динамическая, номинативная и параметрическая. Обобщённые типы могут быть параметризованы, а иерархические отношения между типами явным образом объявляются, а не подразумеваются по совместимой структуре. Одной из особенно отличительных черт системы типов Julia является то, что конкретные типы не могут быть подтипами друг друга: все конкретные типы являются конечными и могут иметь только абстрактные типы в качестве своих надтипов. Хотя это на первый взгляд может показаться излишне ограничительно, это имеет много положительных последствий с весьма небольшим количеством недостатков. Оказывается, что способность наследоваться поведения гораздо важнее, чем способность наследоваться структуры, а наследование обоих приводит к значительным трудностям в традиционных объектно-ориентированных языках. Другие высокоуровневые аспекты системы типов Julia, которые следует упомянуть с самого начала:

  • Нет разделения между объектами и не-объектными значениями: все значения в Julia являются истинными объектами, имеющими тип, принадлежащий одной полностью связанной графе типов, все узлы которой одинаково являются типами первого класса.
  • Нет осмысленного понятия «типа во время компиляции»: единственный тип значения — это его фактический тип во время выполнения программы. Это называется «типом во время выполнения» в объектно-ориентированных языках, где сочетание статической компиляции с полиморфизмом делает это различие значимым.
  • Типы принадлежат только значениям, а не переменным — переменные просто имена, привязанные к значениям.
  • И абстрактные, и конкретные типы могут быть параметризованы другими типами. Они также могут быть параметризованы символами, значениями любого типа, для которого isbits возвращает true (по существу, такие вещи, как числа и булевы значения, которые хранятся как типы C или struct без указателей на другие объекты), а также кортежами из них. Параметры типа могут быть опущены, если они не нуждаются в ссылке или ограничении.

Система типов Julia разработана для того, чтобы быть мощной и выразительной, но ясной, интуитивной и незаметной. Многие программисты Julia могут никогда не чувствовать необходимости писать код, который явно использует типы. Однако некоторые виды программирования становятся более ясными, простыми, быстрыми и более надёжными с объявленными типами.

Объявления типов

Оператор :: может использоваться для добавления аннотаций типа к выражениям и переменным в программах. Есть две основные причины для этого:

  1. В качестве утверждения для подтверждения того, что ваша программа работает так, как вы ожидаете,
  2. Для предоставления дополнительной информации о типе компилятору, который затем может улучшить производительность в некоторых случаях

Когда добавляется к выражению, вычисляющему значение, оператор :: читается как «является экземпляром». Его можно использовать в любом месте для утверждения, что значение выражения слева является экземпляром типа справа. Когда тип справа является конкретным, значение слева должно иметь этот тип в качестве реализации — помните, что все конкретные типы являются конечными, поэтому никакая реализация не является подтипом какой-либо другой. Когда тип является абстрактным, достаточно, чтобы значение реализовывалось конкретным типом, который является подтипом абстрактного типа. Если утверждение типа неверно, выбрасывается исключение, в противном случае возвращается значение левой части:

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got Int64

julia> (1+2)::Int
3

Это позволяет добавить утверждение типа к любому выражению непосредственно.

Когда добавляется к переменной в левой части присваивания или как часть объявления local, оператор :: имеет немного другое значение: он объявляет переменную как имеющую указанный тип всегда, как объявление типа в статически типизированном языке, таком как C. Каждое значение, присвоенное переменной, будет преобразовано в объявленный тип с помощью convert:

julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> foo()
100

julia> typeof(ans)
Int8

Эта функция полезна для избежания «подводных камней» производительности, которые могут возникнуть, если одно из присваиваний переменной изменит её тип неожиданно.

Это поведение «объявления» происходит только в определённых контекстах:

local x::Int8  # in a local declaration
x::Int8 = 10   # as the left-hand side of an assignment

и применяется ко всему текущему пространству имён, даже до объявления. В настоящее время объявления типов нельзя использовать в глобальном пространстве имён, например, в REPL, так как в Julia ещё нет глобальных переменных постоянного типа.

Объявления также могут быть присоединены к определениям функций:

function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

Возврат из этой функции ведет себя так же, как присваивание переменной с объявленным типом: значение всегда преобразуется в Float64.

Абстрактные типы

Абстрактные типы не могут быть инстанцированы и служат только узлами в графе типов, тем самым описывая наборы связанных конкретных типов: те конкретные типы, которые являются их потомками. Мы начинаем с абстрактных типов, даже несмотря на то, что у них нет инстанциаций, потому что они являются основой системы типов: они формируют концептуальную иерархию, которая делает систему типов Julia чем-то большим, чем просто коллекцию реализаций объектов.

Напомним, что в Целых и числах с плавающей точкой мы представили различные конкретные типы числовых значений: Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, и Float64. Несмотря на то, что они имеют различные размеры представления, Int8, Int16, Int32, Int64 и Int128 все они являются знаковыми целыми типами. Аналогично UInt8, UInt16, UInt32, UInt64 и UInt128 являются беззнаковыми целыми типами, в то время как Float16, Float32 и Float64 различны, будучи типами с плавающей точкой, а не целыми. Часто фрагмент кода имеет смысл, например, только если его аргументы — это какой-то вид целого числа, но фактически не зависит от того, какой именно вид целого числа. Например, алгоритм наибольшего общего делителя работает для всех видов целых чисел, но не будет работать для чисел с плавающей точкой. Абстрактные типы позволяют строить иерархию типов, предоставляя контекст, в который могут вписываться конкретные типы. Это позволяет вам, например, легко программировать для любого типа, являющегося целым числом, не ограничивая алгоритм конкретным типом целого числа.

Абстрактные типы объявляются с помощью ключевого слова abstract type. Общие синтаксические конструкции для объявления абстрактного типа:

abstract type «name» end
abstract type «name» <: «supertype» end

Ключевое слово abstract type вводит новый абстрактный тип, чьё имя задаётся «name». Это имя может быть необязательно после <: и уже существующего типа, указывая, что недавно объявленный абстрактный тип является подтипом этого «родительского» типа.

Когда не указан надтип, по умолчанию надтипом является Any — предопределённый абстрактный тип, экземплярами которого являются все объекты, и все типы являются подтипами. В теории типов Any обычно называется «верхним», так как он находится в вершине графа типов. Julia также имеет предопределённый абстрактный «нижний» тип, находящийся в нижней части графа типов, который записывается как Union{}. Он представляет собой полную противоположность Any: ни один объект не является экземпляром Union{} и все типы являются надтипами Union{}.

Рассмотрим некоторые абстрактные типы, составляющие числовую иерархию Julia:

abstract type Number end
abstract type Real     <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer  <: Real end
abstract type Signed   <: Integer end
abstract type Unsigned <: Integer end

Тип Number является прямым дочерним типом Any, а Real — его дочерним. В свою очередь, Real имеет двух дочерних типов (их больше, но здесь показаны только два; мы дойдем до остальных позже): Integer и AbstractFloat, разделяя мир на представления целых чисел и представлений действительных чисел. Представления действительных чисел, конечно же, включают типы с плавающей точкой, но также включают и другие типы, такие как рациональные числа. Следовательно, AbstractFloat является надтипом Real, включающим только представления действительных чисел с плавающей точкой. Целые числа далее подразделяются на Signed и Unsigned разновидности.

Оператор <: в общем случае означает "является подтипом", и, используемый в объявлениях такого рода, указывает, что тип справа является непосредственным супертипом вновь объявленного типа. Он также может использоваться в выражениях как оператор подтипов, возвращающий true, когда его левый операнд является подтипом правого операнда:

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

Важное применение абстрактных типов заключается в предоставлении реализаций по умолчанию для конкретных типов. Для простоты рассмотрим:

function myplus(x,y)
    x+y
end

Первое, что следует отметить, это то, что вышеуказанные объявления аргументов эквивалентны x::Any и y::Any. Когда эта функция вызывается, скажем, как myplus(2,5), диспетчер выбирает наиболее специфичный метод с именем myplus, который соответствует заданным аргументам. (См. Методы для получения дополнительной информации о множественной диспетчеризации.)

Предположим, что не найдено метода, более специфичного, чем указанный выше. В этом случае Julia неявно определяет и компилирует метод с именем myplus специально для двух Int аргументов на основе обобщенной функции, приведенной выше, т.е. неявно определяет и компилирует:

function myplus(x::Int,y::Int)
    x+y
end

и, наконец, вызывает этот специфический метод.

Таким образом, абстрактные типы позволяют программистам писать обобщенные функции, которые впоследствии могут использоваться в качестве метода по умолчанию многими комбинациями конкретных типов. Благодаря множественной диспетчеризации программист имеет полный контроль над тем, используется ли метод по умолчанию или более специфический метод.

Следует отметить, что нет потери производительности, если программист полагается на функцию, аргументы которой являются абстрактными типами, поскольку она перекомпилируется для каждой комбинации конкретных типов аргументов, с которыми она вызывается. (Однако может возникнуть проблема производительности в случае аргументов функции, которые являются контейнерами абстрактных типов; см. Рекомендации по производительности.)

Примитивные типы

Примитивный тип — это конкретный тип, данные которого состоят из обычных битов. Классическими примерами примитивных типов являются целые числа и значения с плавающей точкой. В отличие от большинства языков, Julia позволяет объявлять свои собственные примитивные типы, а не предоставлять только фиксированный набор встроенных. На самом деле, все стандартные примитивные типы определены в самом языке:

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end

primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end

primitive type Int8    <: Signed   8 end
primitive type UInt8   <: Unsigned 8 end
primitive type Int16   <: Signed   16 end
primitive type UInt16  <: Unsigned 16 end
primitive type Int32   <: Signed   32 end
primitive type UInt32  <: Unsigned 32 end
primitive type Int64   <: Signed   64 end
primitive type UInt64  <: Unsigned 64 end
primitive type Int128  <: Signed   128 end
primitive type UInt128 <: Unsigned 128 end

Общие синтаксисы для объявления примитивного типа:

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

Количество битов указывает, сколько памяти требуется для типа, а имя присваивает новое имя типу. Примитивный тип можно необязательно объявить как подтип некоторого супертипа. Если супертип опущен, то тип по умолчанию имеет Any в качестве непосредственного супертипа. Объявление Bool выше, таким образом, означает, что для хранения булевого значения требуется восемь битов, и его непосредственным супертипом является Integer. В настоящее время поддерживаются только размеры, кратные 8 битам. Поэтому булевы значения, хотя на самом деле им нужен всего один бит, не могут быть объявлены меньше восьми битов.

Типы Bool, Int8 и UInt8 все имеют идентичное представление: они являются восьмибитными кусками памяти. Однако, поскольку система типов Julia номинативная, они не взаимозаменяемы, несмотря на идентичную структуру. Основное различие между ними заключается в том, что у них разные супертипы: непосредственный супертип Bool — Integer, Int8 — Signed, а UInt8 — Unsigned. Все остальные различия между Bool, Int8 и UInt8 — это вопросы поведения — способ определения функций для работы с объектами этих типов в качестве аргументов. Вот почему необходима номинативная система типов: если структура определяла тип, а тип, в свою очередь, диктовал поведение, то было бы невозможно заставить Bool вести себя иначе, чем Int8 или UInt8.

Составные типы

Составные типы называются записями, структурами или объектами в различных языках. Составной тип — это набор именованных полей, экземпляр которого можно рассматривать как одно значение. Во многих языках составные типы являются единственным видом определяемых пользователем типов, и они являются, безусловно, наиболее часто используемым типом, определяемым пользователем, в Julia.

В распространённых объектно-ориентированных языках, таких как C++, Java, Python и Ruby, составные типы также имеют связанные с ними именованные функции, и это сочетание называется «объектом». В более чистых объектно-ориентированных языках, таких как Ruby или Smalltalk, все значения являются объектами, независимо от того, являются ли они составными или нет. В менее чистых объектно-ориентированных языках, включая C++ и Java, некоторые значения, такие как целые числа и значения с плавающей точкой, не являются объектами, в то время как экземпляры определяемых пользователем составных типов являются истинными объектами с связанными методами. В Julia все значения являются объектами, но функции не объединены с объектами, над которыми они работают. Это необходимо, так как Julia выбирает, какой метод функции использовать, с помощью множественной диспетчеризации, что означает, что типы *всех* аргументов функции учитываются при выборе метода, а не только первого (см. Методы для получения дополнительной информации о методах и диспетчеризации). Таким образом, нецелесообразно, чтобы функции «принадлежали» только своему первому аргументу. Организация методов в объекты функций, а не именованные наборы методов «внутри» каждого объекта, в конечном итоге является очень полезным аспектом дизайна языка.

Составные типы вводятся с помощью ключевого слова struct, за которым следует блок имён полей, необязательно аннотированных типами с помощью оператора :::

julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

Поля без аннотации типа по умолчанию имеют тип Any, и соответственно могут содержать любые значения.

Новые объекты типа Foo создаются путем применения объекта типа Foo как функции к значениям для его полей:

julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

Когда тип применяется как функция, он называется *конструктором*. Автоматически генерируются два конструктора (они называются *конструкторами по умолчанию*). Один принимает любые аргументы и вызывает convert для преобразования их в типы полей, а другой принимает аргументы, точно соответствующие типам полей. Причина генерации обоих конструкторов заключается в том, что это облегчает добавление новых определений без непреднамеренной замены конструктора по умолчанию.

Поскольку поле bar не ограничено типом, подойдёт любое значение. Однако значение для baz должно быть преобразуемо в Int:

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]

Список имён полей можно получить с помощью функции fieldnames.

julia> fieldnames(Foo)
(:bar, :baz, :qux)

Вы можете получить доступ к значениям полей составного объекта, используя традиционную запись foo.bar:

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

Составные объекты, объявленные с помощью ключевого слова struct являются *неизменяемыми*; они не могут быть изменены после создания.

  • Это может быть эффективнее. Некоторые структуры могут быть эффективно упакованы в массивы, и в некоторых случаях компилятор может вообще избежать выделения неизменяемых объектов.
  • Невозможно нарушить инварианты, предоставляемые конструкторами типа.
  • Код, использующий неизменяемые объекты, может быть проще для понимания.

Неизменяемый объект может содержать изменяемые объекты, такие как массивы, в качестве полей. Эти содержащиеся объекты останутся изменяемыми; только поля самого неизменяемого объекта не могут быть изменены для указания на другие объекты.

В случае необходимости, изменяемые составные объекты могут быть объявлены с ключевым словом mutable struct, о котором будет сказано в следующей секции.

Неизменяемые составные типы без полей являются одиночными экземплярами; может быть только один экземпляр таких типов:

julia> struct NoFields
       end

julia> NoFields() === NoFields()
true

Функция === подтверждает, что «два» созданных экземпляра NoFields на самом деле являются одним и тем же. Одиночные типы описаны более подробно ниже.

Есть много информации о том, как создаются экземпляры составных типов, но это обсуждение зависит как от Параметрических типов, так и от Методов, и достаточно важно, чтобы оно было рассмотрено в отдельной секции: Конструкторы.

Изменяемые составные типы

Если составной тип объявлен с ключевым словом mutable struct вместо struct, то его экземпляры можно изменять:

julia> mutable struct Bar
           baz
           qux::Float64
       end

julia> bar = Bar("Hello", 1.5);

julia> bar.qux = 2.0
2.0

julia> bar.baz = 1//2
1//2

Для поддержки мутаций такие объекты обычно выделяются в куче и имеют стабильные адреса памяти. Изменяемый объект — это небольшой контейнер, который может содержать разные значения с течением времени, и поэтому его можно надежно идентифицировать только по его адресу. В отличие от этого, экземпляр неизменяемого типа связан с конкретными значениями полей — сами значения полей полностью описывают объект. При решении вопроса о том, следует ли сделать тип изменяемым, спросите себя, будут ли два экземпляра с одинаковыми значениями полей считаться идентичными или они должны изменяться независимо с течением времени. Если они будут считаться идентичными, тип, вероятно, должен быть неизменяемым.

Вкратце, два основных свойства определяют неизменяемость в Julia:

  • Изменять значение неизменяемого типа запрещено.
    • Для типов битов это означает, что битовое представление значения, однажды заданное, никогда не изменится, и это значение является идентичностью типа битов.
    • Для составных типов это означает, что идентичность значений его полей никогда не изменится. Когда поля являются типами битов, это означает, что их биты никогда не изменятся, а для полей, значениями которых являются изменяемые типы, такие как массивы, это означает, что поля всегда будут ссылаться на то же изменяемое значение, даже если содержимое этого изменяемого значения само может быть изменено.
  • Объект с неизменяемым типом может быть свободно скопирован компилятором, поскольку его неизменяемость делает невозможным программное различие между исходным объектом и копией.
    • В частности, это означает, что достаточно малые неизменяемые значения, такие как целые числа и числа с плавающей точкой, обычно передаются функциям в регистрах (или выделенных в стеке).
    • Изменяемые значения, с другой стороны, выделяются в куче и передаются функциям как указатели на значения, выделенные в куче, за исключением случаев, когда компилятор уверен, что нет способа определить, что этого не происходит.

Объявленные типы

Три вида типов (абстрактные, примитивные, составные), обсуждавшиеся в предыдущих разделах, на самом деле тесно связаны. Они обладают одинаковыми ключевыми свойствами:

  • Они явно объявляются.
  • У них есть имена.
  • У них есть явно объявленные супертипы.
  • У них могут быть параметры.

Благодаря этим общим свойствам, эти типы внутренне представляются как экземпляры одной и той же концепции, DataType, которая является типом любого из этих типов:

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

DataType может быть абстрактным или конкретным. Если он конкретный, он имеет указанный размер, структуру хранения и (необязательно) имена полей. Таким образом, примитивный тип является DataType с ненулевым размером, но без имён полей. Составной тип является DataType, который имеет имена полей или пуст (нулевой размер).

Каждое конкретное значение в системе является экземпляром некоторого DataType.

Объединения типов

Объединение типов — это специальный абстрактный тип, который включает в себя все экземпляры любого из его аргументных типов, созданный с использованием специального ключевого слова Union:

julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}

julia> 1 :: IntOrString
1

julia> "Hello!" :: IntOrString
"Hello!"

julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got Float64

Компиляторы многих языков имеют внутреннюю конструкцию объединений для рассуждений о типах; Julia просто предоставляет её программисту. Компилятор Julia способен генерировать эффективный код при наличии типов Union с небольшим количеством типов [1], генерируя специализированный код в отдельных ветвях для каждого возможного типа.

Особо полезным случаем типа Union является Union{T, Nothing}, где T может быть любым типом, а Nothing — это одиночный тип, единственным экземпляром которого является объект nothing. Этот шаблон является аналогом Julia Nullable, Option или Maybe типов в других языках. Объявление аргумента функции или поля как Union{T, Nothing} позволяет установить его либо в значение типа T, либо в nothing для указания отсутствия значения. Для получения дополнительной информации см. эту запись в FAQ.

Параметрические типы

Важной и мощной особенностью системы типов Julia является параметричность: типы могут принимать параметры, так что объявления типов фактически вводят целую семью новых типов — по одному для каждой возможной комбинации значений параметров. Существует множество языков, которые поддерживают некоторую версию обобщённого программирования, в котором структуры данных и алгоритмы для их обработки могут быть определены без указания точных типов. Например, некоторая форма обобщённого программирования существует в ML, Haskell, Ada, Eiffel, C++, Java, C#, F# и Scala, чтобы назвать лишь некоторые. Некоторые из этих языков поддерживают истинную параметрическую полиморфию (например, ML, Haskell, Scala), в то время как другие поддерживают ситуативные, основанные на шаблонах стили обобщённого программирования (например, C++, Java). При таком многообразии стилей обобщённого программирования и параметрических типов в различных языках мы даже не будем пытаться сравнивать параметрические типы Julia с другими языками, а сосредоточимся на объяснении системы Julia в её собственном контексте. Тем не менее, мы отметим, что поскольку Julia является динамически типизированным языком и не нуждается в принятии всех решений о типах во время компиляции, многие традиционные трудности, возникающие в статических системах параметрических типов, могут быть относительно легко решены.

Все объявленные типы (разновидность DataType ) могут быть параметризованы с одинаковым синтаксисом в каждом случае. Мы будем обсуждать их в следующем порядке: сначала параметрические составные типы, затем параметрические абстрактные типы и, наконец, параметрические примитивные типы.

Параметрические составные типы

Параметры типа вводятся сразу после имени типа, заключённые в фигурные скобки:

julia> struct Point{T}
           x::T
           y::T
       end

Это объявление определяет новый параметрический тип, Point{T}, содержащий две «координаты» типа T. Что, можно спросить, представляет T? Это именно суть параметрических типов: он может быть любым типом вообще (или значением любого типа битов, на самом деле, хотя здесь он явно используется как тип). Point{Float64} — это конкретный тип, эквивалентный типу, определённому путём подстановки T в определении Point на Float64. Таким образом, это одно объявление фактически объявляет неограниченное число типов: Point{Float64}, Point{AbstractString}, Point{Int64}, и так далее. Каждый из них теперь является применимым конкретным типом:

julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

Тип Point{Float64} — это точка, координаты которой — 64-битные значения с плавающей запятой, а тип Point{AbstractString} — это «точка», «координаты» которой — строковые объекты (см. Строки).

Point сам по себе также является допустимым объектом типа, содержащим все экземпляры Point{Float64}, Point{AbstractString}, и так далее, как подтипы:

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

Другие типы, конечно, не являются подтипами:

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

Конкретные типы Point с разными значениями T никогда не являются подтипами друг друга:

julia> Point{Float64} <: Point{Int64}
false

julia> Point{Float64} <: Point{Real}
false
Предупреждение

Этот последний момент очень важен: даже если Float64 <: Real у нас НЕ БУДЕТ Point{Float64} <: Point{Real}.

Другими словами, в терминологии теории типов, параметры типа Julia являются *неизменными*, а не ковариантными (или даже контравариантными). Это по практическим соображениям: хотя любой экземпляр Point{Float64} концептуально может быть похожим на экземпляр Point{Real} тоже, два типа имеют разные представления в памяти:

  • Экземпляр Point{Float64} может быть представлен компактно и эффективно как немедленная пара 64-битных значений;
  • Экземпляр Point{Real} должен иметь возможность содержать любую пару экземпляров Real. Поскольку объекты, являющиеся экземплярами Real, могут иметь произвольный размер и структуру, на практике экземпляр Point{Real} должен быть представлен парой указателей на индивидуально выделенные объекты Real.

Эффективность, полученная благодаря возможности хранения объектов Point{Float64} с немедленными значениями, многократно увеличивается в случае массивов: массив Array{Float64} может храниться как непрерывный блок памяти 64-битных значений с плавающей запятой, тогда как массив Array{Real} должен быть массивом указателей на индивидуально выделенные объекты Real — которые могут быть как «упакованными» 64-битными значениями с плавающей запятой, так и произвольно большими, сложными объектами, объявленными как реализации абстрактного типа Real.

Так как Point{Float64} не является подтипом Point{Real}, следующий метод не может быть применён к аргументам типа Point{Float64}:

function norm(p::Point{Real})
    sqrt(p.x^2 + p.y^2)
end

Правильный способ определения метода, принимающего все аргументы типа Point{T} где T является подтипом Real:

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

(Эквивалентно, можно определить function norm(p::Point{T} where T<:Real) или function norm(p::Point{T}) where T<:Real; см. UnionAll Типы.)

Более подробные примеры будут рассмотрены позже в Методах.

Как создать объект Point? Можно определить пользовательские конструкторы для составных типов, которые будут подробно рассмотрены в Конструкторах, но в отсутствие специальных объявлений конструкторов есть два стандартных способа создания новых составных объектов, один из которых явно задаёт параметры типа, а другой — подразумевает их из аргументов конструктора объекта.

Так как тип Point{Float64} — это конкретный тип, эквивалентный Point объявленному с Float64 вместо T, он может быть использован как конструктор соответствующим образом:

julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(ans)
Point{Float64}

Для стандартного конструктора для каждого поля должен быть предоставлен ровно один аргумент:

julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
[...]

julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
[...]

Для параметрических типов генерируется только один стандартный конструктор, так как его переопределение невозможно. Этот конструктор принимает любые аргументы и преобразует их в типы полей.

Во многих случаях избыточно указывать тип объекта Point , который нужно создать, так как типы аргументов вызова конструктора уже неявно предоставляют информацию о типе. По этой причине вы также можете использовать Point как конструктор, при условии, что подразумеваемое значение параметра типа T однозначно:

julia> Point(1.0,2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(ans)
Point{Float64}

julia> Point(1,2)
Point{Int64}(1, 2)

julia> typeof(ans)
Point{Int64}

В случае Point, тип T однозначно подразумевается только тогда, когда два аргумента Point имеют один и тот же тип. Если это не так, конструктор завершится с ошибкой MethodError:

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
  Point(::T, !Matched::T) where T at none:2

Методы конструкторов для корректной обработки таких смешанных случаев можно определить, но это не будет обсуждаться до дальнейшего рассмотрения в Конструкторах.

Параметрические абстрактные типы

Декларации параметрических абстрактных типов объявляют набор абстрактных типов аналогичным образом:

julia> abstract type Pointy{T} end

С этим объявлением, Pointy{T} представляет собой отдельный абстрактный тип для каждого типа или целого значения T. Как и в случае параметрических составных типов, каждый такой экземпляр является подтипом Pointy:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

Параметрические абстрактные типы являются инвариантными, так же как и параметрические составные типы:

julia> Pointy{Float64} <: Pointy{Real}
false

julia> Pointy{Real} <: Pointy{Float64}
false

Запись Pointy{<:Real} может быть использована для выражения аналога ковариантного типа в 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: in Pointy, in T, expected T<:Real, got Type{AbstractString}

julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Int64

Параметры типа для параметрических составных типов можно ограничить аналогичным образом:

struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

Чтобы привести реальный пример того, как вся эта параметрическая машина типов может быть полезна, вот фактическое определение неизменяемого типа 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 используются для представления аргументов, принимаемых методами varargs (см. Функции Varargs).

Тип Vararg{T,N} соответствует ровно N элементам типа T. NTuple{N,T} является удобным псевдонимом для Tuple{Vararg{T,N}}, т. е. кортежного типа, содержащего ровно N элементов типа T.

Типы именованных кортежей

Именованные кортежи являются экземплярами типа NamedTuple, который имеет два параметра: кортеж символов, задающих имена полей, и кортежный тип, задающий типы полей.

julia> typeof((a=1,b="hello"))
NamedTuple{(:a, :b),Tuple{Int64,String}}

Тип NamedTuple может использоваться в качестве конструктора, принимающего один кортежный аргумент. Построенный тип NamedTuple может быть либо конкретным типом с указанием обоих параметров, либо типом, который указывает только имена полей:

julia> NamedTuple{(:a, :b),Tuple{Float32, String}}((1,""))
(a = 1.0f0, b = "")

julia> NamedTuple{(:a, :b)}((1,""))
(a = 1, b = "")

Если типы полей указаны, аргументы преобразуются. В противном случае используются типы аргументов напрямую.

Типы-синглтоны

Здесь необходимо упомянуть особый вид абстрактных параметрических типов: типы-синглтоны. Для каждого типа T, «тип-синглтон» Type{T} — это абстрактный тип, единственным экземпляром которого является объект T. Поскольку определение немного сложно анализировать, давайте рассмотрим несколько примеров:

julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Float64})
false

julia> isa(Real, Type{Real})
true

julia> isa(Float64, Type{Real})
false

Другими словами, isa(A,Type{B}) истинно тогда и только тогда, когда A и B являются одним и тем же объектом, и этот объект является типом. Без параметра Type — это просто абстрактный тип, у которого все типы объектов являются экземплярами, включая, конечно, и типы-синглтоны:

julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

Любой объект, который не является типом, не является экземпляром Type:

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

Пока мы не обсудим Параметрические методы и преобразования, сложно объяснить полезность конструкции типа-синглтона, но коротко — она позволяет специализировать поведение функции на конкретных значениях типа. Это полезно для написания методов (особенно параметризованных), поведение которых зависит от типа, заданного как явный аргумент, а не выведенного из типа одного из его аргументов.

Несколько популярных языков имеют типы-синглтоны, включая Haskell, Scala и Ruby. В общем использовании термин «тип-синглтон» относится к типу, единственным экземпляром которого является одно значение. Это значение применимо к типам-синглтонам Julia, но с оговоркой, что только объекты типов имеют типы-синглтоны.

Параметрические примитивные типы

Примитивные типы также могут быть объявлены параметрически. Например, указатели представлены как примитивные типы, которые объявляются в Julia следующим образом:

# 32-bit system:
primitive type Ptr{T} 32 end

# 64-bit system:
primitive type Ptr{T} 64 end

Несколько необычная особенность этих объявлений по сравнению с типичными параметризованными составными типами заключается в том, что параметр типа T не используется в определении самого типа — это просто абстрактная метка, по существу определяющая целую семью типов с идентичной структурой, отличающихся только своим параметром типа. Таким образом, Ptr{Float64} и Ptr{Int64} являются различными типами, несмотря на то, что они имеют идентичное представление. И, конечно же, все конкретные типы указателей являются подтипами универсального типа Ptr:

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

Типы UnionAll

Мы говорили, что параметрический тип, подобный Ptr выступает в качестве супертипа всех его экземпляров (Ptr{Int64} и т. д.). Как это работает? Ptr сам по себе не может быть обычным типом данных, поскольку без знания типа ссылаемых данных тип явно не может использоваться для операций с памятью. Ответ заключается в том, что Ptr (или другие параметрические типы, такие как Array ) — это другой тип, называемый типом UnionAll. Такой тип выражает итерационное объединение типов для всех значений некоторого параметра.

UnionAll типы обычно записываются с использованием ключевого слова where. Например, Ptr можно более точно записать как Ptr{T} where T, что означает все значения, тип которых Ptr{T} для некоторого значения T. В этом контексте параметр T также часто называется «переменной типа», поскольку он, как переменная, принимает значения из типов. Каждый where вводит одну переменную типа, поэтому эти выражения вложены для типов с несколькими параметрами, например, Array{T,N} where N where T.

Синтаксис применения типа A{B,C} требует, чтобы A был типом UnionAll, и сначала подставляет B в самую внешнюю переменную типа в A. Результат должен быть другим типом UnionAll, в который затем подставляется C . Таким образом, A{B,C} эквивалентно A{B}{C}. Это объясняет, почему можно частично инстанциировать тип, как в Array{Float64}: первое значение параметра фиксировано, но второе все еще принимает все возможные значения. Используя явный синтаксис where , можно зафиксировать любой подмножество параметров. Например, тип всех одномерных массивов можно записать как Array{T,1} where T.

END_OF_DOCUMENT_MARKER

Переменные типов могут быть ограничены отношениями подтипов. Array{T} where T<:Integer относится ко всем массивам, тип элементов которых — какой-либо Integer. Синтаксис Array{<:Integer} является удобным сокращением для Array{T} where T<:Integer. Переменные типов могут иметь как нижние, так и верхние границы. Array{T} where Int<:T<:Number относится ко всем массивам Number, которые могут содержать Int (поскольку T должен быть по крайней мере таким же большим, как Int). Синтаксис where T>:Int также используется для указания только нижней границы переменной типа, а Array{>:Int} эквивалентно Array{T} where T>:Int.

Поскольку выражения where вложены, границы переменных типа могут ссылаться на внешние переменные типа. Например, Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real относится к 2-кортежам, первый элемент которых — какое-то Real, а второй элемент — Array любого массива, тип элементов которого содержит тип первого элемента кортежа.

Ключевое слово where само по себе может быть вложено внутри более сложного объявления. Например, рассмотрим два типа, созданных следующими объявлениями:

julia> const T1 = Array{Array{T,1} where T, 1}
Array{Array{T,1} where T,1}

julia> const T2 = Array{Array{T,1}, 1} where T
Array{Array{T,1},1} where T

Тип T1 определяет одномерный массив одномерных массивов; каждый из внутренних массивов состоит из объектов одного и того же типа, но этот тип может изменяться от одного внутреннего массива к другому. С другой стороны, тип T2 определяет одномерный массив одномерных массивов, все внутренние массивы которых должны иметь один и тот же тип. Обратите внимание, что T2 — это абстрактный тип, например, Array{Array{Int,1},1} <: T2, в то время как T1 — это конкретный тип. Вследствие этого, T1 может быть создан с конструктором без аргументов a=T1(), но T2 не может.

Существует удобный синтаксис для именования таких типов, аналогичный сокращённой форме синтаксиса определения функций:

Vector{T} = Array{T,1}

Это эквивалентно const Vector = Array{T,1} where T. Запись Vector{Float64} эквивалентна записи Array{Float64,1}, а универсальный тип Vector имеет в качестве экземпляров все Array объекты, где второй параметр — количество измерений массива — равен 1, независимо от типа элемента. В языках, где параметрические типы всегда должны быть указаны полностью, это не особенно полезно, но в Julia это позволяет написать просто Vector для абстрактного типа, включающего все одномерные плотные массивы любого типа элементов.

Псевдонимы типов

Иногда удобно ввести новое имя для уже выразимого типа. Это можно сделать с помощью простого оператора присваивания. Например, UInt является псевдонимом либо UInt32, либо UInt64, в зависимости от размера указателей в системе:

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

Это достигается с помощью следующего кода в base/boot.jl:

if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

Конечно, это зависит от того, к чему Int привязан — но это предварительно определённый правильный тип — либо Int32, либо Int64.

(Обратите внимание, что в отличие от Int, Float не существует в качестве псевдонима типа для конкретного AbstractFloat фиксированного размера. В отличие от целочисленных регистров, где размер Int отражает размер родного указателя на данной машине, размеры регистров с плавающей точкой задаются стандартом IEEE-754.)

Операции над типами

Поскольку типы в Julia — это сами объекты, обычные функции могут работать с ними. Некоторые функции, которые особенно полезны для работы с типами или изучения типов, уже были введены, например, оператор <:, который указывает, является ли левое операнд подтипом правого операнда.

Функция isa проверяет, является ли объект заданного типа, и возвращает true или false:

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

Функция typeof, уже используемая на протяжении всей документации в примерах, возвращает тип своего аргумента. Поскольку, как отмечалось выше, типы являются объектами, они также имеют типы, и мы можем спросить, какими они являются:

julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,String})
Union

Что если мы повторим процесс? Каков тип типа типа? Как ни странно, типы — это все составные значения, и поэтому все имеют тип DataType:

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType — это его собственный тип.

Ещё одна операция, которая применяется к некоторым типам, — supertype, которая показывает супертип типа. Только объявленные типы (DataType) имеют однозначные супертипы:

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

Если вы примените supertype к другим объектам типа (или не объектам типа), будет поднята ошибка MethodError:

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
  supertype(!Matched::DataType) at operators.jl:42
  supertype(!Matched::UnionAll) at operators.jl:47

Настройка красивого вывода

Часто нужно настроить отображение экземпляров типа. Это достигается перегрузкой функции show. Например, предположим, что мы определили тип для представления комплексных чисел в полярной форме:

julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar

Здесь мы добавили пользовательскую функцию-конструктор, чтобы она могла принимать аргументы разных типов Real и преобразовывать их в общий тип (см. Конструкторы и Преобразование и продвижение). (Конечно, нам нужно было бы определить и другие методы, чтобы он действовал как Number, например, +, *, one, zero, правила продвижения и так далее.) По умолчанию экземпляры этого типа отображаются довольно просто, с информацией о имени типа и значениях полей, например, Polar{Float64}(3.0,4.0).

Если мы хотим, чтобы он отображался как 3.0 * exp(4.0im), мы бы определили следующий метод для вывода объекта в заданный объект вывода io (представляющий файл, терминал, буфер и так далее; см. Сети и потоки):

julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

Более точный контроль над отображением объектов Polar возможен. В частности, иногда требуется как многострочный формат вывода, используемый для отображения отдельного объекта в REPL и других интерактивных средах, так и более компактный однострочный формат, используемый для print или для отображения объекта как части другого объекта (например, в массиве). Хотя по умолчанию функция show(io, z) вызывается в обоих случаях, вы можете определить другой многострочный формат для отображения объекта, перегрузив трёхаргументную форму show, принимающую MIME-тип text/plain в качестве второго аргумента (см. Мультимедийный ввод/вывод), например:

julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
           print(io, "Polar{$T} complex number:\n   ", z)

(Обратите внимание, что print(..., z) здесь вызовет метод show(io, z) с 2 аргументами.) Это приводит к:

julia> Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Array{Polar{Float64},1}:
 3.0 * exp(4.0im)
 4.0 * exp(5.3im)

где однострочный формат show(io, z) по-прежнему используется для массива значений Polar. Технически, REPL вызывает display(z) для отображения результата выполнения строки, который по умолчанию устанавливается в show(stdout, MIME("text/plain"), z), который, в свою очередь, по умолчанию устанавливается в show(stdout, z), но вы не должны определять новые методы display, если вы не определяете новый обработчик мультимедийного отображения (см. Мультимедийный ввод/вывод).

Кроме того, вы также можете определить методы show для других MIME-типов, чтобы включить более подробное отображение (HTML, изображения и т. д.) объектов в средах, которые это поддерживают (например, IJulia). Например, мы можем определить отформатированное отображение HTML объектов Polar, с надстрочными и курсивными элементами, посредством:

julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
           println(io, "<code>Polar{$T}</code> complex number: ",
                   z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")

Объект Polar будет автоматически отображаться с помощью HTML в среде, поддерживающей HTML-отображение, но вы можете вызвать show вручную, чтобы получить HTML-вывод, если хотите:

julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>

HTML-рендерер отобразит это как: Polar{Float64} комплексное число: 3,0 e4,0 i

В качестве общего правила, однострочный метод show должен печатать допустимое выражение Julia для создания показанного объекта. Когда этот метод show содержит инфиксные операторы, такие как оператор умножения (*) в нашем однострочном методе show для Polar выше, он может не распознаваться при выводе в качестве части другого объекта. Чтобы увидеть это, рассмотрим объект выражения (см. Представление программы), который берёт квадрат определённого экземпляра нашего типа Polar:

julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2

Поскольку оператор ^ имеет более высокий приоритет, чем * (см. Приоритет и ассоциативность операторов), этот вывод не верно отображает выражение a ^ 2, которое должно быть равно (3.0 * exp(4.0im)) ^ 2. Чтобы решить эту проблему, мы должны создать пользовательский метод для Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int), который вызывается внутри объекта выражения при выводе:

julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
           if Base.operator_precedence(:*) <= precedence
               print(io, "(")
               show(io, z)
               print(io, ")")
           else
               show(io, z)
           end
       end

julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)

Определённый выше метод добавляет скобки вокруг вызова show в том случае, если приоритет вызываемого оператора выше или равен приоритету умножения. Эта проверка позволяет выражениям, которые распознаются верно без скобок (например, :($a + 2) и :($a == 2) ), опускать их при выводе:

julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)

julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)

В некоторых случаях полезно настраивать поведение методов show в зависимости от контекста. Это можно сделать с помощью типа IOContext, который позволяет передавать контекстные свойства вместе с обернутым потоком IO. Например, мы можем создать более короткое представление в нашем методе show, когда свойство :compact установлено в значение true, возвращаясь к длинному представлению, если свойство false или отсутствует:

julia> function Base.show(io::IO, z::Polar)
           if get(io, :compact, false)
               print(io, z.r, "ℯ", z.Θ, "im")
           else
               print(io, z.r, " * exp(", z.Θ, "im)")
           end
       end

Это новое компактное представление будет использоваться, когда передаваемый поток IO является объектом IOContext с установленным свойством :compact. В частности, это имеет место при выводе массивов с несколькими столбцами (где горизонтальное пространство ограничено):

julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im

julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Array{Polar{Float64},2}:
 3.0ℯ4.0im  4.0ℯ5.3im

См. документацию IOContext для списка общих свойств, которые можно использовать для настройки вывода.

"Типы значений"

В Julia вы не можете диспатчить по значению, такому как true или false. Однако вы можете диспатчить по параметрическим типам, и Julia позволяет вам включать значения «простых битов» (Типы, Символы, Целые числа, числа с плавающей точкой, кортежи и т. д.) в качестве параметров типа. Типичный пример — параметр размерности в Array{T,N}, где T — это тип (например, Float64), но N — просто Int.

Вы можете создавать свои собственные пользовательские типы, которые принимают значения в качестве параметров, и использовать их для управления диспатчем пользовательских типов. Для иллюстрации этой идеи давайте представим параметрический тип Val{x} и конструктор Val(x) = Val{x}(), который служит обычным способом использования этой техники в случаях, когда вам не нужна более сложная иерархия.

Val определен как:

julia> struct Val{x}
       end

julia> Val(x) = Val{x}()
Val

Реализация Val на этом заканчивается. Некоторые функции стандартной библиотеки Julia принимают экземпляры Val в качестве аргументов, и вы также можете использовать их для написания собственных функций. Например:

julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)

julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)

julia> firstlast(Val(true))
"First"

julia> firstlast(Val(false))
"Last"

Для согласованности в Julia место вызова всегда должно передавать экземпляр Valэкземпляра, а не тип, т. е. используйте foo(Val(:bar)) вместо foo(Val{:bar}).

Стоит отметить, что очень легко неправильно использовать параметрические «типы значений», включая Val; в неблагоприятных случаях вы легко можете значительно ухудшить производительность своего кода. В частности, вам никогда не следует писать реальный код, как показано выше. Для получения дополнительной информации о правильном (и неправильном) использовании Val, пожалуйста, ознакомьтесь с более подробным обсуждением в разделе советов по производительности.

[1]

«Маленький» определяется константой MAX_UNION_SPLITTING, которая в настоящее время установлена в 4.

© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.2.0/manual/types/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API