Spec-Zone.ru › Julia 1.8

Типы

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

Система типов 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 a value of type Int64

julia> (1+2)::Int
3

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

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

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

julia> x = foo()
100

julia> typeof(x)
Int8

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

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

function myplus(x,y)
    x+y
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

julia> typeof(foo)
Foo

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

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

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

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

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

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

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

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

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

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

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

Если все поля неизменяемой структуры неразличимы (===) то два неизменяемых значения, содержащие эти поля, также неразличимы:

julia> struct X
           a::Int
           b::Float64
       end

julia> X(1, 2) === X(1, 2)
true

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

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

Если составной тип объявлен с ключевым словом 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:

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

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

const аннотирование полей изменяемых структур требует как минимум Julia 1.8.

julia> mutable struct Baz
           a::Int
           const b::Float64
       end

julia> baz = Baz(1, 1.5);

julia> baz.a = 2
2

julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed
[...]

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

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

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

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

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

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

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

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

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

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

julia> 1 :: IntOrString
1

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

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

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

Особый полезный случай Union типа — Union{T, Nothing}, где T может быть любым типом, а Nothing — это одиночный тип, единственный экземпляр которого — объект nothing. Эта модель является эквивалентом 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 Types.)

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

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

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

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

julia> typeof(p)
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> p1 = Point(1.0,2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(p1)
Point{Float64}

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

julia> typeof(p2)
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 a value of type 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, которое обозначает любое количество последующих элементов:

julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString, Vararg{Int64}}

julia> isa(("1",), mytupletype)
true

julia> isa(("1",1), mytupletype)
true

julia> isa(("1",1,2), mytupletype)
true

julia> isa(("1",1,2,3.0), mytupletype)
false

Кроме того, Vararg{T} соответствует нулю или более элементам типа T. Типы кортежей с произвольным числом аргументов используются для представления аргументов, принимаемых методами с произвольным числом аргументов (см. Функции с произвольным числом аргументов).

Специальное значение Vararg{T,N} (когда используется в качестве последнего параметра типа кортежа) соответствует ровно N элементам типа T. NTuple{N,T} — это удобный псевдоним для Tuple{Vararg{T,N}}, т.е. тип кортежа, содержащий ровно N элементов типа T.

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

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

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

Макрос @NamedTuple предоставляет более удобный struct-подобный синтаксис для объявления NamedTuple типов через key::Type объявления, где опущенное ::Type соответствует ::Any.

julia> @NamedTuple{a::Int, b::String}
NamedTuple{(:a, :b), Tuple{Int64, String}}

julia> @NamedTuple begin
           a::Int
           b::String
       end
NamedTuple{(:a, :b), Tuple{Int64, String}}

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

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

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

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

Параметризованные примитивные типы

Примитивные типы также могут быть объявлены параметрически. Например, указатели представляются примитивными типами, которые будут объявлены в 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}
Vector{Vector} (alias for Array{Array{T, 1} where T, 1})

julia> const T2 = Array{Array{T, 1}, 1} where T
Array{Vector{T}, 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 для абстрактного типа, включающего все одномерные плотные массивы любого типа элементов.

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

Неизменяемые составные типы без полей называются сингултонами. Формально, если

  1. T — это неизменяемый составной тип (т.е. определён с struct ),
  2. a isa T && b isa T подразумевает a === b ,

то T является типом-сингултоном.[2] Base.issingletontype можно использовать для проверки, является ли тип типом-сингултоном. Абстрактные типы по определению не могут быть типами-сингултонами.

Из определения следует, что таких типов может быть только один экземпляр:

julia> struct NoFields
       end

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

julia> Base.issingletontype(NoFields)
true

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

Параметрические типы могут быть типами-сингултонами, когда выполняется указанное выше условие. Например,

julia> struct NoFieldsParam{T}
       end

julia> Base.issingletontype(NoFieldsParam) # can't be a singleton type ...
false

julia> NoFieldsParam{Int}() isa NoFieldsParam # ... because it has ...
true

julia> NoFieldsParam{Bool}() isa NoFieldsParam # ... multiple instances
true

julia> Base.issingletontype(NoFieldsParam{Int}) # parametrized, it is a singleton
true

julia> NoFieldsParam{Int}() === NoFieldsParam{Int}()
true

Типы функций

Каждая функция имеет свой тип, который является подтипом Function.

julia> foo41(x) = x + 1
foo41 (generic function with 1 method)

julia> typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

Обратите внимание, как typeof(foo41) выводится как само собой. Это просто соглашение для вывода, так как это объект первого класса, который можно использовать как любое другое значение:

julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

julia> T <: Function
true

Типы функций, определённых на верхнем уровне, являются типами-сингултонами. При необходимости их можно сравнить с ===.

Замыкания также имеют свой тип, который обычно выводится с именами, оканчивающимися на #<number>. Имена и типы функций, определённых в разных местах, различны, но не гарантируется, что они будут выводиться одинаково в разных сессиях.

julia> typeof(x -> x + 1)
var"#9#10"

Типы замыканий необязательно являются типами-сингултонами.

julia> addy(y) = x -> x + y
addy (generic function with 1 method)

julia> Base.issingletontype(addy(1))
false

julia> addy(1) === addy(2)
false

Type{T} селекторы типов

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

Поскольку определение немного сложно для анализа, давайте рассмотрим несколько примеров:

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

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

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

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

Другими словами, isa(A, Type{B}) истинно тогда и только тогда, когда A и B — это один и тот же объект, и этот объект — тип.

В частности, поскольку параметрические типы неизменны, у нас есть

julia> struct TypeParamExample{T}
           x::T
       end

julia> TypeParamExample isa Type{TypeParamExample}
true

julia> TypeParamExample{Int} isa Type{TypeParamExample}
false

julia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}
true

Без параметра 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

Хотя Type является частью иерархии типов Julia, как и любой другой абстрактный параметрический тип, он не используется часто вне сигнатур методов, за исключением некоторых особых случаев. Ещё одно важное применение Type — уточнение типов полей, которые в противном случае были бы захвачены менее точно, например, как DataType в примере ниже, где конструктор по умолчанию может привести к проблемам производительности в коде, полагающемся на точный тип обертки (аналогично параметрам абстрактных типов).

julia> struct WrapType{T}
       value::T
       end

julia> WrapType(Float64) # default constructor, note DataType
WrapType{DataType}(Float64)

julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)
WrapType

julia> WrapType(Float64) # sharpened constructor, note more precise Type{Float64}
WrapType{Type{Float64}}(Float64)

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

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

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

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

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

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

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

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

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

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

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

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

julia> typeof(Rational{Int})
DataType

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

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

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

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

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

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

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

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
[...]

Настройка вывода в красивом формате

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

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

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

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

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

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

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

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

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

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

julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Vector{Polar{Float64}}:
 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 Matrix{Polar{Float64}}:
 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.
  • 2Некоторые популярные языки имеют типы-синглетоны, включая Haskell, Scala и Ruby.

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

Spec-Zone.ru

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