Типы

В этой главе я отталкиваюсь от официального руководства (http://docs.julialang.org/en/stable/manual/types/\). Это близко к переводу с сокращениями, с редкими вставками моих комментариев, оформленных курсивом.

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

Система типов в Julia характеризуется тремя определениями: динамическая, номинативная и параметрическая. Ниже кратко объясним их суть.

Динамическая: как во многих динамических языках, в Julia типы привязаны к конкретным значениям, а не к переменным.

Номинативная: тип определяется именем (в противоположность системам, где тип идентифицируется в первую очередь его структурой).

Параметрическая: общие типы могут быть параметризованы.

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

Авторы языка сошлись во мнении, что важно наследование поведения, и не важно наследование структуры. Хотя обсуждения на тему изменения/дополнения этой системы поднимались пару лет назад (это то, что я видел на github, где разработчики обсуждают подобные вопросы), это в конечном счете не привело до сих пор к какой-то переделке. По моему личному мнению, здесь не помешали бы трейты в стиле Scala (это тоже обсуждалось и, может быть, еще появится), хотя, как я мог убедится на практике, возможность параметризовать типы может удовлетворить многие потребности разработчика.

Некоторы высокоуровневые аспекты, которые следует упомянуть сразу:

  • Нет различия между объктом и "не-объектом": все значения - настоящие оъекты, имеющие свой тип.
  • Нет понятия "тип врмени компиляции": единственным типом значения является ее тип, когда программа запущена (тип времени выполнения).
  • Только значения, а не переменные, имеют типы: переменные просто имена, связанные со значениями.
  • И абстрактные и конкретные типы могут быть параметризованы другими типами, символами (Symbols), и значениями любого типа, для которого isbits() возвращает true, а также их кортежами. Типовые параметры могут быть опущены, когда нет необходимости в их уточнении/ограничении.

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

Объявления типа ::

Для аннотации типов используется оператор ::. Его использование сводится к двум случаям.

  1. Будучи добавленным к вычисленному выражению, он понимается как "является экземпляром типа". Эта форма может быть использована везде, где есть желание ввести утверждение-проверку (typeassert), позволяющее убедиться что тип выражения тот, который вы ожидаете. При невыполнении условия утверждения возникает ошибка. Если указанный тип является абстрактным, то выражение слева от него должно быть одним из его подтипов. Если указан конкретный тип, он выражение может быть только этого типа:
julia> (1+2)::AbstractFloat
ERROR: TypeError: typeassert: expected AbstractFloat, got Int64
 ...

julia> (1+2)::Int
3
  1. Будучи использовано а) с переменной, которой присваивается значение (x::Int8 = 10) или б) как часть объявления local( local x::Int8 ) оператор :: устанавливает, что указанная переменная всегда будет иметь этот тип (подобно объявлению типа переменной в статически-типизированных языках). Каждое присваиваемое этой переменной значение, если оно другого типа, будет преобразовано в требуемый тип с помощью функции convert(). (Объявляемые в REPL глобальные переменные не поддерживают эту возможность).
julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> foo()
100

julia> typeof(ans)
Int8

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

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

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

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

abstract «name»
abstract «name» <: «supertype»

Если супертип не задан - он прнимается равным Any. Оператор <: здесь читается как "является подтипом" (левое подтипом правого). Пример, того как в самом языке описывается иерархия абстрактных числовых типов:

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

Оператор <: может использоваться в тексте программы как условный оператор проверки типов, который возвращает true, если левый опреанд является подтипом правого (или является тем же типом):

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

julia> Int64<:Int64
true

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

Рассмотрим пример, пусть мы объявили функцию:

function myplus(x,y)
    x+y
end

Это объявление метода функции myplus на двух аргументах типа Any.

Когда мы вызовем эту функцию: myplus(2,5), диспетчер будет искать наиболее конкретную реализацию myplus для двух аргументов типа Int (Int - это псевдоним, указывающий, например, на Int64). За неимением более конкретного, будет скомпилирована реализация:

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

и, наконец, он вызывает эту конкретную реализвцию.

Если вы определите метод:

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

то, при вызове myplus(2,5) диспетчер будет использовать вашу более конкретную реализацию:

myplus(2,5)
14

Хотя для другой комбинации типов будет вызвана более общая реализация:

myplus(2, 5.0)
7.0

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

Битовые типы (Bits Types)

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

bitstype 16 Float16 <: AbstractFloat
bitstype 32 Float32 <: AbstractFloat
bitstype 64 Float64 <: AbstractFloat

bitstype 8  Bool <: Integer
bitstype 32 Char

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

Синтаксис для объявлении bitstype:

bitstype «bits» «name»
bitstype «bits» «name» <: «supertype»

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

Типы Bool, INT8 и Uint8 все имеют одинаковые представления: они Восьмибитовое куски памяти. Так как система типа Julia номинативная, они не являются взаимозаменяемыми, несмотря на одинаковую структуру.

Составные типы (Composite Types)

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

type Foo
    bar
    baz::Int
    qux::Float64
end

Без аннотации типа (как bar), поле может иметь значение любого типа (подразумевается аннотация типа Any)

Новые объекты создаются слкдующим образом:

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

julia> typeof(foo)
Foo

Когда тип так применяется (как функция), он называется конструктором. Автоматически создаются два конструктора по умолчанию: один принимает все типы параметров и применяет к ним, при необходимости, функцию convert(). Второй принимает только точно совпадающие типы аргументов. Это позволяет объявлять свои дополнительные конструкторы без опасений, что сломается конструктор по умолчанию.

Узнать список полей можно с помощью функции fieldnames:

julia> fieldnames(foo)
3-element Array{Symbol,1}:
 :bar
 :baz
 :qux

Доступ к полям объекта:

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

Можно изменять значения полей:

julia> foo.qux = 2
2

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

Составной тип без полей является синглтоном: может быть создан только один его экземпляр:

type NoFields
end

julia> is(NoFields(), NoFields())
true

Неизменяемые составные типы (Immutable Composite Types)

Неизменяемый тип определяется так:

immutable Complex
    real::Float64
    imag::Float64
end

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

  • Они более эффективны в некоторых случаях. Типы, такие как пример выше могут быть упакованы эффективно в массивы, а в некоторых случаях компилятор способен оптимизировать выделение памяти.
  • Нельзя нарушить инварианты, предусмотренные конструкторами типа.
  • О них легче рассуждать.

Неизменяемый объект может содержать изменяемые объекты, такие как массивы. Сами по себе эти объекты (их содержимое) останутся изменчивыми.

В операторах присваивания и вызовах функций неизменяемые объекты передаются путем копирования. (а изменяемые - копированием ссылки).

Тип типов

Т.к. в Julia сами типы являются, в свою очередь, объектами. То и у них должен быть свой тип. Этим типом является DataType:

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

Объединение типов (Union)

Указать, что значение может быть одним из нескольких типов можно с помощью конструктора Union:

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

julia> 1 :: IntOrString
1

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

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

Параметрические типы (Parametric Types)

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

type Point{T}
    x::T
    y::T
end

Здесь тип Point может принимать типовой параметр T. Примерами конкретного типа будут: Point{Float64} или Point{Int64} или даже Point{AbstractString}. Однако Point сам по себе - корректный тип:

julia> Point
Point{T}

Конкретный тип является подтипом параметрического

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

но Float64 не является подтипом Point:

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} неверно.

Если нужно определить, что параметр может иметь не только конкретный тип, а и его подтипы, то нужно делать так:

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

Это читается так: для функции norm определим, что некий некий тип, назовем его T, является подтипом (или типом ) Real, а параметр p должен быть типом 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 указывает, что оба его поля должны быть одного типа. Поэтому, если сделаем так, то получим ошибку:

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point{T}(::Int64, ::Float64)

Абстрактные параметрические типы (Parametric Abstract Types)

Аналогичным образом можно объявить типовые параметры у абстрактного типа:

abstract Pointy{T}

и точно так же, как с составными типами:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

и они так же инвариантны:

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

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

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

type Point{T} <: Pointy{T}
    x::T
    y::T
end

Если объект точки (Point) не должен содержать не числовые поля, то можно сделать так:

abstract Pointy{T<:Real}

при этом:

julia> Pointy{Float64}
Pointy{Float64}

julia> Pointy{Real}
Pointy{Real}

julia> Pointy{AbstractString}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Type{AbstractString}
 ...

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

потом сделаем так:

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

Кортежные типы (Tuple Types)

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

  • Они могут иметь любое количество параметров.
  • Они ковариантны, то есть: Tuple{Int} является подтипом 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 Tuple Types)

Последним аргументом кортежного типа может быть особый тип Vararg. Он означает любое количество завершающих параметров.

julia> isa(("1",), Tuple{AbstractString,Vararg{Int}})
true

julia> isa(("1",1), Tuple{AbstractString,Vararg{Int}})
true

julia> isa(("1",1,2), Tuple{AbstractString,Vararg{Int}})
true

julia> isa(("1",1,2,3.0), Tuple{AbstractString,Vararg{Int}})
false

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

Типы-одиночки ( Singleton Types )

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

Определение .

Для любого типа T тип-одиночка Type{T} - это абстрактный тип, чей единственный экземпляр является объектом T.

Понять это можно так: в Julia не только любое значение является объектом (определенного типа), но и сами типы объектов являются объектами... но какого типа? Тип любого типа - это DataType. Но, он не информативен. Нужен способ описать тип, чтобы можно было использовать эту информацию позже. Нужное, как раз описывается типом Type{T}.

Теперь, зачем это нужно на примере работы функций. Как мы знаем, функция может принимать аргументы. Кроме самого значения, она имеет доступ и к типу переменной, например, с помощью typeof():

f(x) = "Значение $x типа $(typeof(x))

Некоторые функции не используют полученное значение аргумента, так как им важен лишь его тип значения:

f(::AbstractString) = "Со строками не работаем"|>error

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

julia> do_you_work_with( AbstractString )
false

julia> do_you_work_with( Int )
true

Как же как же может выглядеть определение функции, принимающей вместо значения - только тип? А вот как:

do_you_work_with{T<:AbstractString}( x::Type{T}) = false

do_you_work_with{T<:Number}( x::Type{T}) = true

Возвращаюсь к документации.

Функция isa(x, type) -> Bool - возвращает true, когда x является значением типа type.

Пример:

julia> isa(1,Int)
true

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

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

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

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

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

Будучи не параметризованным тип Type является типом любого типа:

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

julia> isa(Float64,Type)
true

julia> isa(Real,Type)
true

Операции на типах

Поскольку типы в Julia сами по себе являются объектами, обычные функции могут работать с ними. Есть уже готовые функции, например, <: проверяющая , что левый операнд является подтипом правого.typeof() возвращает тип своего аргумента. supertype() возвращает супертип своего аргумента для декларированных типов (объектов типа DataType). Для не типовых объектов она вернет ошибку:

julia> supertype(Union{Float64,Int64})
ERROR: `supertype` has no method matching supertype(::Type{Union{Float64,Int64}})

Типы-значения ( “Value types” )

Julia не позволяет определять диспетчеризацию по значениям. Но она позволяет это делать по параметрическим типам , и вы можете использовать "простые битовые" значения (Types, Symbols, Integers, floating-point numbers, tuples, etc.) как типовые параметры. Типичным примером является параметр размерности в Array{T,N}, где Т представляет собой тип (например, Float64), но N является только Int. Вы можете создавать свои собственные типы, которые принимают значения в качестве параметров, и использовать их для управления диспетчеризацией пользовательских типов. В качестве иллюстрации этой идеи, давайте введем параметрический тип, Val{T}, который служит способом использовать эту технику для тех случаев, когда вам не нужна более сложная иерархия. Val объявлен так:

immutable Val{T}
end

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

firstlast(::Type{Val{true}}) = "First"
firstlast(::Type{Val{false}}) = "Last"

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

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

Обратите внимание, что используется тип, в общем виде: foo(Val{:bar}), вместо создания экземпляра: foo(Val{:bar}())

Но, нужно быть внимательным, применяя Val. Можно нечаянно снизить производительность своего кода, неправильным его использованием. Прочитайте советы по повышению производительности прежде чем его применить (http://docs.julialang.org/en/stable/manual/performance-tips/\#man-performance-val ).

Nullable

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

Чтобы создать отсутствующее значение, нужно сделать так:

julia> x1 = Nullable{Int64}()
Nullable{Int64}()

julia> x2 = Nullable{Float64}()
Nullable{Float64}()

julia> x3 = Nullable{Vector{Int64}}()
Nullable{Array{Int64,1}}()

Создать НЕ отсутствующее значение, можно так:

julia> x1 = Nullable(1)
Nullable{Int64}(1)
julia> x2 = Nullable(1.0)
Nullable{Float64}(1.0)

julia> x3 = Nullable([1, 2, 3])
Nullable{Array{Int64,1}}([1,2,3])

Проверить наличие/отсутствие значение в Nullable можно так:

julia> isnull(Nullable{Float64}())
true

julia> isnull(Nullable(0.0))
false

Доступ к значению, (ради него все и затевалось):

julia> get(Nullable{Float64}())
ERROR: NullException()

in get(::Nullable{Float64}) at ./nullable.jl:62

...

julia> get(Nullable(1.0))
1.0

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

julia> get(Nullable{Float64}(), 0.0)
0.0

julia> get(Nullable(1.0), 0.0)
1.0

Значение по умолчанию будет автоматически преобразовано в ожидаемый тип.

results matching ""

    No results matching ""