Интерфейсы

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

Итерация

Обязательные методы Краткое описание

iterate(iter)

Возвращает либо кортеж из первого элемента и начального состояния, либо nothing, если пуст

iterate(iter, state)

Возвращает либо кортеж из следующего элемента и следующего состояния, либо nothing, если элементов не осталось

Важные необязательные методы

Определение по умолчанию

Краткое описание

Base.IteratorSize(IterType)

Base.HasLength()

Один из Base.HasLength(), Base.HasShape{N}(), Base.IsInfinite() или Base.SizeUnknown() в зависимости от ситуации

Base.IteratorEltype(IterType)

Base.HasEltype()

Либо Base.EltypeUnknown(), либо Base.HasEltype() в зависимости от ситуации

eltype(IterType)

Any

Тип первой записи кортежа, возвращаемого iterate()

length(iter)

(не определено)

Количество элементов, если известно

size(iter, [dim])

(не определено)

Количество элементов в каждом измерении, если известно

Base.isdone(iter[, state])

missing

Указание быстрого пути для завершения итератора. Должен быть определен для итераторов с сохранением состояния, иначе isempty(iter) может вызвать iterate(iter[, state]) и изменить итератор.

Значение, возвращаемое IteratorSize(IterType) Обязательные методы

Base.HasLength()

length(iter)

Base.HasShape{N}()

length(iter) и size(iter, [dim])

Base.IsInfinite()

(нет)

Base.SizeUnknown()

(нет)

Значение, возвращаемое IteratorEltype(IterType) Обязательные методы

Base.HasEltype()

eltype(IterType)

Base.EltypeUnknown()

(нет)

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

Любой объект, который определяет эта функция, является итерируемым и может быть использован во многих функциях, где применяется итерация. Его также можно использовать непосредственно в цикле for, поскольку синтаксис

for item in iter   # или "for item = iter"
    # тело
end

преобразовывается в

next = iterate(iter)
while next !== nothing
    (item, state) = next
    # тело
    next = iterate(iter, state)
end

Простым примером является итерируемая последовательность квадратов чисел определенной длины:

julia> struct Squares
           count::Int
       end

julia> Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)

Имея только определение iterate, тип Squares уже является довольно мощным. Можно выполнять итерацию всех элементов:

julia> for item in Squares(7)
           println(item)
       end
1
4
9
16
25
36
49

Можно использовать многие встроенные методы, работающие с итерируемыми коллекциями, например in или sum:

julia> 25 in Squares(10)
true

julia> sum(Squares(100))
338350

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

julia> Base.eltype(::Type{Squares}) = Int # Обратите внимание, что это определено для типа

julia> Base.length(S::Squares) = S.count

Теперь когда в Julia требуется собрать все элементы в массив с помощью метода collect, Julia может предварительно выделить Vector{Int} нужного размера вместо того, чтобы отправлять каждый элемент в push! с помощью функции Vector{Any}:

julia> collect(Squares(4))
4-element Vector{Int64}:
  1
  4
  9
 16

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

julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)

julia> sum(Squares(1803))
1955361914

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

Кроме того, часто полезно разрешить итерацию коллекции в обратном порядке путем итерации с помощью функции Iterators.reverse(iterator). Однако для фактической поддержки итерации в обратном порядке тип итератора T должен реализовать iterate для Iterators.Reverse{T}. (Учитывая r::Iterators.Reverse{T}, базовым итератором типа T является r.itr.) В приведенном далее примере Squares реализуются методы Iterators.Reverse{Squares}:

julia> Base.iterate(rS::Iterators.Reverse{Squares}, state=rS.itr.count) = state < 1 ? nothing : (state*state, state-1)

julia> collect(Iterators.reverse(Squares(4)))
4-element Vector{Int64}:
 16
  9
  4
  1

Индексирование

Реализуемые методы Краткое описание

getindex(X, i)

X[i], доступ к индексированному элементу

setindex!(X, v, i)

X[i] = v, индексированное назначение

firstindex(X)

Первый индекс, используемый в X[begin]

lastindex(X)

Последний индекс, используемый в X[end]

Для приведенного выше итерируемого объекта Squares можно легко вычислить i-й элемент последовательности, возведя его в квадрат. Процесс можно представить в виде выражения индексирования S[i]. Чтобы принять такое поведение, Squares просто должен определить функцию getindex:

julia> function Base.getindex(S::Squares, i::Int)
           1 <= i <= S.count || throw(BoundsError(S, i))
           return i*i
       end

julia> Squares(100)[23]
529

Кроме того, для поддержки синтаксиса S[begin] и S[end] необходимо определить firstindex и lastindex, чтобы указать первый и последний допустимые индексы, соответственно:

julia> Base.firstindex(S::Squares) = 1

julia> Base.lastindex(S::Squares) = length(S)

julia> Squares(23)[end]
529

Для многомерного индексирования begin/end, как, например, в a[3, begin, 7], необходимо определить firstindex(a, dim) и lastindex(a, dim) (которые по умолчанию вызывают first и last для axes(a, dim), соответственно).

Заметим, однако, что приведенный выше пример определяет только функцию getindex с одним целочисленным индексом. Индексирование с помощью чего-либо, отличного от Int, приведет к ошибке MethodError, информирующей об отсутствии подходящего метода. Для поддержки индексирования с диапазонами или векторами Int необходимо написать отдельные методы:

julia> Base.getindex(S::Squares, i::Number) = S[convert(Int, i)]

julia> Base.getindex(S::Squares, I) = [S[i] for i in I]

julia> Squares(10)[[3,4.,5]]
3-element Vector{Int64}:
  9
 16
 25

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

Абстрактные массивы

Реализуемые методы Краткое описание

size(A)

Возвращает кортеж, содержащий измерения A

getindex(A, i::Int)

(если IndexLinear) Линейное скалярное индексирование

getindex(A, I::Vararg{Int, N})

(если IndexCartesian, где N = ndims(A)) N-мерное скалярное индексирование

Необязательные методы

Определение по умолчанию

Краткое описание

IndexStyle(::Type)

IndexCartesian()

Возвращает IndexLinear() или IndexCartesian(). См. описание ниже.

setindex!(A, v, i::Int)

(если IndexLinear) Скалярное индексированное назначение

setindex!(A, v, I::Vararg{Int, N})

(если IndexCartesian, где N = ndims(A)) N-мерное скалярное индексированное назначение

getindex(A, I...)

определяется с точки зрения скалярной функции getindex

Многомерное и нескалярное индексирование

setindex!(A, X, I...)

определяется с точки зрения скалярной функции setindex!

Многомерное и нескалярное индексированное назначение

iterate

определяется с точки зрения скалярной функции getindex

Итерация

length(A)

prod(size(A))

Количество элементов

similar(A)

similar(A, eltype(A), size(A))

Возвращает изменяемый массив с той же формой и типом элементов

similar(A, ::Type{S})

similar(A, S, size(A))

Возвращает изменяемый массив с той же формой и указанным типом элементов

similar(A, dims::Dims)

similar(A, eltype(A), dims)

Возвращает изменяемый массив с тем же типом элементов и измерениями размера

similar(A, ::Type{S}, dims::Dims)

Array{S}(undef, dims)

Возвращает изменяемый массив с указанным типом элементов и размером

Нетрадиционные индексы

Определение по умолчанию

Краткое описание

axes(A)

map(OneTo, size(A))

Возвращает кортеж AbstractUnitRange{<:Integer} из допустимых индексов

similar(A, ::Type{S}, inds)

similar(A, S, Base.to_shape(inds))

Возвращает изменяемый массив с указанными индексами inds (см. ниже)

similar(T::Union{Type,Function}, inds)

T(Base.to_shape(inds))

Возвращает массив, аналогичный T, с указанными индексами inds (см. ниже)

Если тип определен как подтип AbstractArray, он наследует очень большой набор разнообразных возможностей, включая итерацию и многомерное индексирование, созданное на базе одноэлементного доступа. Дополнительные сведения о поддерживаемых методах см. на странице руководства, посвященной массивам, и в этом разделе Julia Base.

Ключевым элементом в определении подтипа AbstractArray является тип IndexStyle. Поскольку индексирование является важной частью массива и часто встречается в горячих циклах, важно максимально эффективно выполнять как индексирование, так и индексированное назначение. Структуры данных массивов обычно определяются одним из двух способов: либо они максимально эффективно получают доступ к своим элементам, используя только один индекс (линейное индексирование), либо они внутренним образом получают доступ к элементам с индексами, заданными для каждого измерения. Эти два варианта в Julia называются IndexLinear() и IndexCartesian(). Преобразование линейного индекса в несколько нижних индексов обычно является очень дорогостоящей операцией, поэтому существует основанный на признаках механизм, позволяющий создавать эффективный универсальный код для всех типов массивов.

Это различие определяет, какие методы скалярного индексирования должен определять тип. Массивы IndexLinear() просты: достаточно определить getindex(A::ArrayType, i::Int). Когда массив впоследствии индексируется с помощью многомерного набора индексов, резервный getindex(A::AbstractArray, I...) преобразует индексы в один линейный индекс и затем вызывает вышеуказанный метод. Массивам IndexCartesian(), с другой стороны, требуется определять методы для каждой поддерживаемой размерности с индексами ndims(A) Int. Например, тип SparseMatrixCSC из модуля стандартной библиотеки SparseArrays поддерживает только два измерения, поэтому он просто определяет getindex(A::SparseMatrixCSC, i::Int, j::Int). То же самое относится и к setindex!.

Если взять последовательность квадратов, приведенную выше, ее можно определить как подтип AbstractArray{Int, 1}:

julia> struct SquaresVector <: AbstractArray{Int, 1}
           count::Int
       end

julia> Base.size(S::SquaresVector) = (S.count,)

julia> Base.IndexStyle(::Type{<:SquaresVector}) = IndexLinear()

julia> Base.getindex(S::SquaresVector, i::Int) = i*i

Обратите внимание: очень важно указать два параметра AbstractArray. Первый определяет функцию eltype, а второй — функцию ndims. Этот супертип и эти три метода — все, что нужно, для того, чтобы SquaresVector стал итерируемым, индексируемым и полностью функциональным массивом:

julia> s = SquaresVector(4)
4-element SquaresVector:
  1
  4
  9
 16

julia> s[s .> 8]
2-element Vector{Int64}:
  9
 16

julia> s + s
4-element Vector{Int64}:
  2
  8
 18
 32

julia> sin.(s)
4-element Vector{Float64}:
  0.8414709848078965
 -0.7568024953079282
  0.4121184852417566
 -0.2879033166650653

Возьмем более сложный пример и определим собственный модельный N-мерный разреженный тип массива, созданный на основе Dict:

julia> struct SparseArray{T,N} <: AbstractArray{T,N}
           data::Dict{NTuple{N,Int}, T}
           dims::NTuple{N,Int}
       end

julia> SparseArray(::Type{T}, dims::Int...) where {T} = SparseArray(T, dims);

julia> SparseArray(::Type{T}, dims::NTuple{N,Int}) where {T,N} = SparseArray{T,N}(Dict{NTuple{N,Int}, T}(), dims);

julia> Base.size(A::SparseArray) = A.dims

julia> Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where {T} = SparseArray(T, dims)

julia> Base.getindex(A::SparseArray{T,N}, I::Vararg{Int,N}) where {T,N} = get(A.data, I, zero(T))

julia> Base.setindex!(A::SparseArray{T,N}, v, I::Vararg{Int,N}) where {T,N} = (A.data[I] = v)

Обратите внимание, что это массив IndexCartesian, поэтому нужно вручную определить getindex и setindex! в размерности массива. В отличие от SquaresVector, можно определить setindex! и поэтому можно изменять массив:

julia> A = SparseArray(Float64, 3, 3)
3×3 SparseArray{Float64, 2}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> fill!(A, 2)
3×3 SparseArray{Float64, 2}:
 2.0  2.0  2.0
 2.0  2.0  2.0
 2.0  2.0  2.0

julia> A[:] = 1:length(A); A
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

Результат индексирования AbstractArray сам может быть массивом (например, при индексировании по AbstractRange). Резервные методы AbstractArray используют функцию similar для выделения Array соответствующего размера и типа элементов, который заполняется с помощью базового метода индексирования, описанного выше. Однако при реализации оболочки массива часто требуется, чтобы результат также был заключен в оболочку:

julia> A[1:2,:]
2×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0

В приведенном примере это достигается путем определения Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where T для создания соответствующего заключенного в оболочку массива. (Обратите внимание, что, хотя similar поддерживает одно- и двухаргументные формы, в большинстве случаев нужно специализировать только трехаргументную форму.) Для этого важно, чтобы SparseArray был изменяемым (поддерживал setindex!). Определив similar, getindex и setindex! для SparseArray, скопировать массив с помощью функции copy:

julia> copy(A)
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

Помимо всех приведенных выше итерируемых и индексируемых методов, эти типы также могут взаимодействовать друг с другом и использовать большинство методов, определенных в Julia Base для AbstractArrays:

julia> A[SquaresVector(3)]
3-element SparseArray{Float64, 1}:
 1.0
 4.0
 9.0

julia> sum(A)
45.0

Если вы определяете тип массива, который допускает нетрадиционное индексирование (индексы, начинающиеся со значения, отличного от 1), необходимо специализировать axes. Также следует специализировать similar, чтобы аргумент dims (обычно кортеж размеров Dims) мог принимать объекты AbstractUnitRange, возможно, диапазонные типы Ind вашей собственной разработки. Дополнительные сведения см. в главе Массивы с пользовательскими индексами.

Массивы с заданным шагом

Реализуемые методы Краткое описание

strides(A)

Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в каждом измерении в виде кортежа. Если A является AbstractArray{T,0}, должен быть возвращен пустой кортеж.

Base.unsafe_convert(::Type{Ptr{T}}, A)

Возвращает собственный адрес массива.

Base.elsize(::Type{<:A})

Возвращает шаг между последовательными элементами в массиве.

Необязательные методы

Определение по умолчанию

Краткое описание

stride(A, i::Int)

strides(A)[i]

Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в измерении k.

Массив с заданным шагом — это подтип массива AbstractArray, записи которого хранятся в памяти с фиксированными шагами. При условии, что тип элемента массива совместим с BLAS, массив с заданным шагом может использовать подпрограммы BLAS и LAPACK для более эффективных процедур линейной алгебры. Стандартным примером определяемого пользователем массива с заданным шагом является массив, который заключает стандартный массив Array в дополнительную структуру.

Предупреждение. Не реализуйте эти методы, если базовое хранилище не имеет заданных шагов, так как это может привести к неправильным результатам или ошибкам сегментации.

Ниже приведены примеры типов массивов с заданными шагами и без них.

1:5   # без заданного шага (хранилище, связанное с этим массивом, отсутствует)
Vector(1:5)  # с заданными шагами (1,)
A = [1 5; 2 6; 3 7; 4 8]  # с заданными шагами (1, 4)
V = view(A, 1:2, :)   # с заданными шагами (1, 4)
V = view(A, 1:2:3, 1:2)   # с заданными шагами (2, 4)
V = view(A, [1,2,4], :)   # без заданного шага, так как расстояние между строками не является фиксированным.

Настройка трансляции

Реализуемые методы Краткое описание

Base.BroadcastStyle(::Type{SrcType}) = SrcStyle()

Проведение трансляции типа SrcType

Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})

Выделяет выходной контейнер

Необязательные методы

Base.BroadcastStyle(::Style1, ::Style2) = Style12()

Правила приоритета при смешивании стилей

Base.axes(x)

Объявляет индексы x в соответствии с методом axes(x).

Base.broadcastable(x)

Преобразует x в объект, имеющий axes и поддерживающий индексирование

Обход механизмов по умолчанию

Base.copy(bc::Broadcasted{DestStyle})

Пользовательская реализация broadcast

Base.copyto!(dest, bc::Broadcasted{DestStyle})

Пользовательская реализация broadcast! со специализацией DestStyle

Base.copyto!(dest::DestType, bc::Broadcasted{Nothing})

Пользовательская реализация broadcast! со специализацией DestType

Base.Broadcast.broadcasted(f, args...)

Переопределяет отложенное поведение по умолчанию в выражении с объединением

Base.Broadcast.instantiate(bc::Broadcasted{DestStyle})

Переопределяет вычисления осей отложенной трансляции

Трансляция активируется явным вызовом broadcast или broadcast! либо неявным образом с помощью точечных операций, таких как A .+ b или f.(x, y). Любой объект, имеющий axes и поддерживающий индексирование, может участвовать в качестве аргумента в трансляции. По умолчанию результат сохраняется в Array. Существует три основных способа расширяемости этой базовой платформы:

  • Обеспечение поддержки трансляции всеми аргументами

  • Выбор подходящего выходного массива для заданного набора аргументов

  • Выбор эффективной реализации для заданного набора аргументов

axes и индексирование поддерживаются не всеми типами, но многие можно разрешать в трансляции. Функция Base.broadcastable вызывается для каждого транслируемого аргумента, что позволяет возвращать что-то другое, поддерживающее axes и индексирование. По умолчанию это функция тождества для всех AbstractArray и Number — они уже поддерживают axes и индексирование.

Если тип должен выступать в роли «0-мерного скаляра» (отдельного объекта), а не контейнера для трансляции, то необходимо определить следующий метод:

Base.broadcastable(o::MyType) = Ref(o)

который возвращает аргумент, заключенный в 0-мерный контейнер Ref. Например, такой метод-оболочка определен для самих типов, функций, специальных одинарных объектов, таких как missing и nothing, и дат.

Пользовательские массивоподобные типы могут специализировать Base.broadcastable для определения своей формы, но они должны следовать соглашению о том, что collect(Base.broadcastable(x)) == collect(x). Существенным исключением является AbstractString. Строки имеют специальный регистр, чтобы действовать как скаляры для целей трансляции, хотя они являются итерируемыми коллекциями своих символов (подробнее см. в разделе Строки).

Следующие два шага (выбор выходного массива и реализация) зависят от определения единого решения для заданного набора аргументов. Трансляция должна принимать все разнообразные типы своих аргументов и сводить их к одному выходному массиву и одной реализации. В трансляции это единое решение называется «стилем». Каждый транслируемый объект имеет свой собственный предпочитаемый стиль, а для объединения этих стилей в единое решение — «стиль назначения» — используется система, аналогичная продвижению.

Стили трансляции

Base.BroadcastStyle — это абстрактный тип, производными от которого являются все стили трансляции. При использовании в качестве функции он имеет две возможные формы: унарную (одноаргументную) и бинарную. Унарный вариант означает, что вы намерены реализовать определенное поведение трансляции и (или) тип вывода и не хотите использовать заданный по умолчанию резервный тип Broadcast.DefaultArrayStyle.

Чтобы переопределить эти настройки по умолчанию, можно определить пользовательский тип BroadcastStyle для объекта:

struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()

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

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}() можно использовать для произвольных типов.

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}() является предпочтительным вариантом, если MyType является AbstractArray.

  • Для AbstractArrays, которые поддерживают только определенную размерность, создайте подтип Broadcast.AbstractArrayStyle{N} (см. ниже).

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

Выбор подходящего выходного массива

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

Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})

Резервное определение имеет следующий вид.

similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} =
    similar(Array{ElType}, axes(bc))

Однако при необходимости можно специализировать любой или все эти аргументы. Последний аргумент bc является отложенным представлением (потенциально объединенной) операции трансляции, объектом Broadcasted. Для этих целей наиболее важными полями оболочки являются f и args, описывающие функцию и список аргументов, соответственно. Обратите внимание, что список аргументов может содержать — и часто содержит — другие вложенные оболочки Broadcasted.

Для полного примера: допустим, вы создали тип ArrayAndChar, который хранит массив и один символ:

struct ArrayAndChar{T,N} <: AbstractArray{T,N}
    data::Array{T,N}
    char::Char
end
Base.size(A::ArrayAndChar) = size(A.data)
Base.getindex(A::ArrayAndChar{T,N}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...]
Base.setindex!(A::ArrayAndChar{T,N}, val, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] = val
Base.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'")
# вывод

Может потребоваться, чтобы в ходе трансляции были сохранены «метаданные» char. Сначала определяется

Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()
# вывод

Это означает, что также следует определить соответствующий метод similar:

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType
    # Сканирует входные данные для ArrayAndChar:
    A = find_aac(bc)
    # Использует поле символов A для создания вывода
    ArrayAndChar(similar(Array{ElType}, axes(bc)), A.char)
end

"`A = find_aac(As)` returns the first ArrayAndChar among the arguments."
find_aac(bc::Base.Broadcast.Broadcasted) = find_aac(bc.args)
find_aac(args::Tuple) = find_aac(find_aac(args[1]), Base.tail(args))
find_aac(x) = x
find_aac(::Tuple{}) = nothing
find_aac(a::ArrayAndChar, rest) = a
find_aac(::Any, rest) = find_aac(rest)
# вывод
find_aac (generic function with 6 methods)

Из этих определений вытекает следующее поведение.

julia> a = ArrayAndChar([1 2; 3 4], 'x')
2×2 ArrayAndChar{Int64, 2} with char 'x':
 1  2
 3  4

julia> a .+ 1
2×2 ArrayAndChar{Int64, 2} with char 'x':
 2  3
 4  5

julia> a .+ [5,10]
2×2 ArrayAndChar{Int64, 2} with char 'x':
  6   7
 13  14

Расширение трансляции с помощью пользовательских реализаций

В целом операция трансляции представлена отложенным контейнером Broadcasted, в котором хранится применяемая функция вместе с аргументами. Эти аргументы сами могут быть более глубоко вложенными контейнерами Broadcasted, образуя большое дерево выражений для вычисления. Вложенное дерево контейнеров Broadcasted напрямую формируется с помощью неявного точечного синтаксиса. К примеру, 5 .+ 2.x временно представлен с помощью Broadcasted(+, 5, Broadcasted(, 2, x)). Это действие незаметно для пользователей, так как сразу реализуется через вызов copy, но именно этот контейнер является основой расширяемости трансляции для авторов пользовательских типов. Затем встроенный механизм трансляции определит тип и размер результата на основе аргументов, выделит его и после этого скопирует в него реализацию объекта Broadcasted с помощью метода по умолчанию copyto!(::AbstractArray, ::Broadcasted). Встроенные резервные методы broadcast и broadcast! аналогичным образом формируют временное представление Broadcasted операции, чтобы следовать тому же пути выполнения кода. Это позволяет пользовательским реализациям массивов предоставлять собственную специализацию copyto! для настройки и оптимизации трансляции. Опять же, этот момент определяется вычисляемым стилем трансляции. Это часть операции настолько важна, что она хранится как первый параметр типа Broadcasted, что позволяет осуществлять диспетчеризацию и специализацию.

Для некоторых типов механизмы «объединения» операций на вложенных уровнях трансляции недоступны или могут быть выполнены более эффективно поэтапно. В таких случаях может потребоваться вычислить выражение x .* (x .+ 1), как если бы оно было написано в виде broadcast(, x, broadcast(+, x, 1)), где внутренняя операция вычисляется до выполнения внешней. Этот вид безотложной операции напрямую поддерживается косвенным образом. Вместо непосредственного создания объектов Broadcasted Julia снижает уровень объединенного выражения x . (x .+ 1) до Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1)). Теперь по умолчанию broadcasted просто вызывает конструктор Broadcasted для создания отложенного представления объединенного дерева выражений, но его можно переопределить для определенной комбинации функции и аргументов.

Например, встроенные объекты AbstractRange используют этот механизм для оптимизации частей транслируемых выражений, которые могут безотлагательно вычисляться чисто с точки зрения начала, шага и длины (или остановки) вместо вычисления каждого отдельного элемента. Как и все остальные механизмы, broadcasted также вычисляет и представляет комбинированный стиль трансляции своих аргументов, поэтому вместо специализации broadcasted(f, args...) можно специализировать broadcasted(::DestStyle, f, args...) для любой комбинации стиля, функции и аргументов.

Например, следующее определение поддерживает отрицание диапазонов.

broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))

Расширение трансляции на месте

Трансляцию на месте можно поддерживать, определив соответствующий метод copyto!(dest, bc::Broadcasted). Поскольку может потребоваться специализировать либо dest, либо конкретный подтип bc, во избежание возникновения неоднозначности между пакетами рекомендуется использовать следующее соглашение.

Чтобы специализировать определенный стиль DestStyle, определите метод для

copyto!(dest, bc::Broadcasted{DestStyle})

При необходимости с помощью этой формы можно также специализировать тип dest.

Если вместо него нужно специализировать целевой тип DestType без специализации DestStyle, следует определить метод со следующей сигнатурой.

copyto!(dest::DestType, bc::Broadcasted{Nothing})

Для этого используется резервная реализация copyto!, которая преобразует оболочку в Broadcasted{Nothing}. Следовательно, специализация DestType имеет более низкий приоритет, чем методы, специализирующие DestStyle.

Аналогичным образом можно полностью переопределить трансляцию не на месте с помощью метода copy(::Broadcasted).

Работа с объектами Broadcasted

Для реализации такого метода, как copy или copyto!, необходимо использовать оболочку Broadcasted для вычисления каждого элемента. Это можно сделать двумя основными способами.

  • Broadcast.flatten пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы лично отвечаете за реализацию правил формы трансляции, но это может пригодиться лишь в ограниченных ситуациях.

  • Итерация объекта CartesianIndices метода axes(::Broadcasted) и использование индексирования с результирующим объектом CartesianIndex для вычисления результата.

Написание правил двоичной трансляции

Правила приоритета определяются вызовами двоичного BroadcastStyle:

Base.BroadcastStyle(::Style1, ::Style2) = Style12()

где Style12 — это BroadcastStyle, который нужно выбрать для выводов с аргументами Style1 и Style2. Например,

Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()

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

Для типов AbstractArray определение BroadcastStyle заменяет выбор резервного варианта Broadcast.DefaultArrayStyle. DefaultArrayStyle и абстрактный супертип AbstractArrayStyle хранят размерность в качестве параметра типа для поддержки специализированных типов массивов с требованиями к фиксированной размерности.

DefaultArrayStyle «проигрывает» любому другому AbstractArrayStyle, который был определен следующими методами.

BroadcastStyle(a::AbstractArrayStyle{Any}, ::DefaultArrayStyle) = a
BroadcastStyle(a::AbstractArrayStyle{N}, ::DefaultArrayStyle{N}) where N = a
BroadcastStyle(a::AbstractArrayStyle{M}, ::DefaultArrayStyle{N}) where {M,N} =
    typeof(a)(Val(max(M, N)))

Вам не нужно писать правила двоичного BroadcastStyle, если только вы не хотите установить приоритет для двух или более типов, не являющихся DefaultArrayStyle.

Если тип массива предъявляет требования к фиксированной размерности, следует использовать подтип AbstractArrayStyle. Например, код разреженного массива имеет следующие определения.

struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} end
struct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} end
Base.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()
Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()

Всякий раз, когда подтипом является AbstractArrayStyle, необходимо определять правила для комбинации размерностей, создавая конструктор для стиля, который принимает аргумент Val(N). Пример:

SparseVecStyle(::Val{0}) = SparseVecStyle()
SparseVecStyle(::Val{1}) = SparseVecStyle()
SparseVecStyle(::Val{2}) = SparseMatStyle()
SparseVecStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()

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

Свойства экземпляра

Реализуемые методы Определение по умолчанию Краткое описание

propertynames(x::ObjType, private::Bool=false)

fieldnames(typeof(x))

Возвращает кортеж свойств (x.property) объекта x. Если private=true, также возвращаются имена свойств, которые должны оставаться частными.

getproperty(x::ObjType, s::Symbol)

getfield(x, s)

Возвращает свойство s объекта x. x.s вызывает getproperty(x, :s).

setproperty!(x::ObjType, s::Symbol, v)

setfield!(x, s, v)

Присваивает свойству s объекта x значение v. x.s = v вызывает setproperty!(x, :s, v). Должно возвращаться значение v.

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

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

julia> mutable struct Point
           r::Float64
           ϕ::Float64
       end

julia> p = Point(7.0, pi/4)
Point(7.0, 0.7853981633974483)

Как описывалось в таблице выше, доступ через точечную нотацию p.r равносилен вызову getproperty(p, :r), который по умолчанию равносилен getfield(p, :r):

julia> propertynames(p)
(:r, :ϕ)

julia> getproperty(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)

julia> p.r, p.ϕ
(7.0, 0.7853981633974483)

julia> getfield(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)

Однако нам может потребоваться, чтобы пользователи не знали о том, что координаты в Point хранятся как r и ϕ (поля), а вместо этого взаимодействовали с x и y (свойствами). Можно определить методы в первом столбце, добавив новую функциональность:

julia> Base.propertynames(::Point, private::Bool=false) = private ? (:x, :y, :r, :ϕ) : (:x, :y)

julia> function Base.getproperty(p::Point, s::Symbol)
           if s === :x
               return getfield(p, :r) * cos(getfield(p, :ϕ))
           elseif s === :y
               return getfield(p, :r) * sin(getfield(p, :ϕ))
           else
               # Позволяет обращаться к полям в форме p.r и p.ϕ
               return getfield(p, s)
           end
       end

julia> function Base.setproperty!(p::Point, s::Symbol, f)
           if s === :x
               y = p.y
               setfield!(p, :r, sqrt(f^2 + y^2))
               setfield!(p, :ϕ, atan(y, f))
               return f
           elseif s === :y
               x = p.x
               setfield!(p, :r, sqrt(x^2 + f^2))
               setfield!(p, :ϕ, atan(f, x))
               return f
           else
               # Позволяет изменять поля в форме p.r и p.ϕ
               return setfield!(p, s, f)
           end
       end

Важно использовать внутри getproperty и setproperty! вызовы getfield и setfield вместо точечного синтаксиса, так как точечный синтаксис сделает функции рекурсивными, что может привести к проблемам с выводом типов. Теперь можно испытать новую функциональность:

julia> propertynames(p)
(:x, :y)

julia> p.x
4.949747468305833

julia> p.y = 4.0
4.0

julia> p.r
6.363961030678928

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