Engee documentation

Interfaces

Most of the features and extensibility in Julia are due to a set of informal interfaces. When extending several specific methods to work with a custom type, objects of this type not only receive the appropriate functionality, but can also be used in other methods written to form behavior models.

Iteration

Two mandatory methods are always required.

Mandatory method Short description

iterate(iter)

Returns either a tuple of the first element and the initial state, or nothing, if empty

iterate(iter, state)

Returns either a tuple of the next element and the next state, or nothing if there are no elements left.

There are several other methods that should be determined in specific circumstances. Note that you should always define at least one of the Base' methods.IteratorSize(IterType) and length(iter), since the default is Base.IteratorSize(IterType) is defined as Base.HasLength().

Method When should this method be defined? Default definition Short description

Base.IteratorSize(IterType)

If the default value is not suitable

Base.HasLength()

One of the Base.HasLength(), Base.HasShape{N}(), Base.IsInfinite() or Base.SizeUnknown() depending on the situation

length(iter)

If Base.IteratorSize() returns Base.HasLength() or Base.HasShape{N}()

(not definite)

The number of elements, if known

size(iter, [dim])

If Base.IteratorSize() returns Base.HasShape{N}()

(not definite)

The number of elements in each dimension, if known

Base.IteratorEltype(IterType)

If the default value is not suitable

Base.HasEltype()

Or Base.EltypeUnknown(), or Base.HasEltype() depending on the situation

eltype(IterType)

If the default value is not suitable

Any

The type of the first entry of the tuple returned by iterate()

Base.isdone(iter, [state])

Must be determined if the iterator is monitoring the state

missing

Specifying a fast path to complete the iterator. If not defined for a stateful iterator, readiness-checking functions such as isempty() and `zip()', can change the iterator and lead to errors.

Sequential iteration is implemented using the function iterate. Instead of changing objects as they iterate, Julia iterators can track the iteration state outside of the object. The return value of an iteration is always either a tuple of the value and state, or nothing if there are no elements left. The state object will be passed back to the iteration function at the next iteration and is usually considered an implementation detail private to the iterated object.

Any object that this function defines is iterable and can be used in many functions where iteration is applied. It can also be used directly in a loop. for, because the syntax

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

converted to

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

A simple example is an iterable sequence of squares of numbers of a certain length:

julia> struct Squares
           count::Int
       end

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

Having only a definition iterate, the Squares type is already quite powerful. You can iterate over all the elements.:

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

You can use many built-in methods that work with iterable collections, for example in or sum:

julia> 25 in Squares(10)
true

julia> sum(Squares(100))
338350

There are several other methods that can be extended to provide Julia with more information about this iterable collection. We know that the elements in the Squares' sequence will always be of type `Int'. Extending the method 'eltype, this information can be passed to Julia to create more specialized code in more complex methods. We also know the number of elements in the sequence, so we can also extend the function length:

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

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

Now that Julia requires collecting all the elements in an array using the method collect, Julia can pre-select Vector{Int} the right size instead of sending each item to push! using the Vector' function{Any}:

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

While it is possible to rely on general implementations, it is also possible to extend specific methods if it is known that a simpler algorithm is available in them. For example, there is a formula for calculating the sum of squares, so the universal iterative version can be redefined with a more efficient solution.:

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

julia> sum(Squares(1803))
1955361914

There is a common pattern in Julia Base: a small set of necessary methods defines an informal interface that implements many non-standard behaviors. In some cases, types will need to further specialize these behaviors when they know that a more efficient algorithm can be used in their particular case.

In addition, it is often useful to allow the collection to iterate in_ reverse order by iterating using the function Iterators.reverse(iterator). However, to actually support iteration in reverse, the iterator type T must implement iterate for Iterators.Reverse{T}. (Considering r::Iterators.Reverse{T}, the base iterator of type T is r.itr'.) In the following `Squares example, the Iterators.Reverse' methods are implemented.{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

Indexing

Implemented methods Short description

getindex(X, i)

X[i], indexed access, non-scalar i must allocate a copy

setindex!(X, v, i)

X[i] = v, indexed assignment

firstindex(X)

The first index used in X[begin]

lastindex(X)

The last index used in X[end]

For the iterated Squares object shown above, you can easily calculate the i’th element of the sequence by squaring it. The process can be represented as an indexing expression `S[i]. To accept this behavior, Squares just needs to define a function 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

In addition, to support the syntax of S[begin] and S[end], it is necessary to define firstindex and lastindex to specify the first and last valid indexes, respectively:

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

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

julia> Squares(23)[end]
529

For multidimensional indexing of begin/end', as, for example, in `a[3, begin, 7], it is necessary to define firstindex(a, dim) and lastindex(a, dim) (which by default call first and last for `axes(a, dim)', respectively).

Note, however, that the above example defines only a function getindex with a single integer index. Indexing with anything other than Int will result in an error. MethodError, informing about the absence of a suitable method. To support indexing with ranges or Int vectors, you need to write separate methods.:

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

Although it will support more operations. indexing supported by some built-in types is still missing a number of features. This sequence of Squares' starts to look more and more like a vector as behaviors are added to it. Instead of defining all these behaviors yourself, it can be officially defined as a subtype. `AbstractArray.

Abstract arrays

Implemented methods Short description

size(A)

Returns a tuple containing the dimensions A

getindex(A, i::Int)

(if IndexLinear) Linear scalar indexing

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

(if IndexCartesian', where `N = ndims(A)) N-dimensional scalar indexing

Optional methods

Default definition

Short description

IndexStyle(::Type)

IndexCartesian()

Returns IndexLinear() or `IndexCartesian()'. See the description below.

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

(if IndexLinear) Scalar indexed assignment

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

(if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexed assignment

getindex(A, I...)

it is defined in terms of the scalar function getindex

Multidimensional and non-scalar indexing

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

it is defined in terms of the scalar function setindex!

Multidimensional and non-scalar indexed assignment

iterate

it is defined in terms of the scalar function getindex

Iteration

length(A)

prod(size(A))

Number of elements

similar(A)

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

Returns a mutable array with the same shape and type of elements.

similar(A, ::Type{S})

similar(A, S, size(A))

Returns a mutable array with the same shape and the specified element type.

similar(A, dims::Dims)

similar(A, eltype(A), dims)

Returns a mutable array with the same element type and_size dimensions

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

Array{S}(undef, dims)

Returns a mutable array with the specified element type and size

Non-traditional indexes

Default definition

Short description

axes(A)

map(OneTo, size(A))

Returns the tuple AbstractUnitRange{<:Integer} from valid indexes. The axes must be the proper axes of the index, that is, the condition axes' must be met.(axes(A),1) == axes(A).

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

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

Returns a mutable array with the specified `inds' indexes (see below)

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

T(Base.to_shape(inds))

Returns an array similar to `T', with the specified indexes `inds' (see below)

If a type is defined as a subtype of AbstractArray, it inherits a very large set of diverse features, including iteration and multidimensional indexing based on singleton access. For more information about supported methods, see the manual page dedicated to arrays, and in this section is Julia Base.

The key element in defining the subtype of AbstractArray' is the type `IndexStyle. Since indexing is an important part of the array and is often found in hot loops, it is important to perform both indexing and indexed assignment as efficiently as possible. Array data structures are usually defined in one of two ways: either they access their elements as efficiently as possible using only one index (linear indexing), or they internally access elements with indexes set for each dimension. These two options in Julia are called IndexLinear() and `IndexCartesian()'. Converting a linear index to multiple subscripts is usually a very expensive operation, so there is a feature-based mechanism that allows you to create efficient universal code for all types of arrays.

This difference determines which scalar indexing methods should determine the type. The arrays IndexLinear() are simple: it is enough to define getindex(A::ArrayType, i::Int). When the array is subsequently indexed using a multidimensional set of indexes, the backup getindex(A::AbstractArray, I...) converts the indexes into a single linear index and then calls the above method. IndexCartesian() arrays, on the other hand, require defining methods for each supported dimension with ndims(A) Int indexes. For example, the type SparseMatrixCSC' from the standard library module `SparseArrays' supports only two dimensions, so it simply defines `getindex(A::SparseMatrixCSC, i::Int, j::Int). The same applies to setindex!.

If we take the sequence of squares shown above, it can be defined as a subtype of 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

Please note: it is very important to specify two AbstractArray parameters. The first defines the function 'eltype`, and the second one is a function ndims. This supertype and these three methods are all that is needed for the SquaresVector to become an iterable, indexed, and fully functional array.:

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

Let’s take a more complex example and define our own model N-dimensional sparse array type based on 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)

Note that this is an array of IndexCartesian', so you need to manually define `getindex and setindex! in the dimension of the array. Unlike the SquaresVector', it is possible to define `setindex! and therefore it is possible to modify the array:

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

The result of indexing AbstractArray can itself be an array (for example, when indexing by AbstractRange'). The backup methods of `AbstractArray' use the function 'similar to select an Array of the appropriate size and type of elements, which is filled using the basic indexing method described above. However, when implementing an array wrapper, it is often required that the result is also enclosed in the wrapper.:

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

In the example above, this is achieved by defining Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where T to create the corresponding enclosed array. (Note that although 'similar` supports one- and two-argument forms, in most cases only the three-argument form needs to be specialized.) For this, it is important that SparseArray is mutable (supports setindex!). By defining similar, getindex and setindex! for SparseArray, you can copy the array using the function 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

In addition to all the iterated and indexed methods above, these types can also interact with each other and use most of the methods defined in Julia Base for AbstractArrays:

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

julia> sum(A)
45.0

If you are defining an array type that allows non-traditional indexing (indexes starting with a value other than 1), you need to specialize axes. You should also specialize similar so that the dims argument (usually a tuple of Dims sizes) can accept AbstractUnitRange objects, possibly the Ind range types of your own design. For more information, see the chapter Arrays with custom indexes.

Arrays with a given step

Implemented methods Short description

strides(A)

Returns the distance in memory (in the number of elements) between adjacent elements in each dimension as a tuple. If A is an AbstractArray{T,0}, an empty tuple must be returned.

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

Returns the array’s own address.

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

Returns the step between consecutive elements in an array.

Optional methods

Default definition

Short description

stride(A, i::Int)

strides(A)[i]

Returns the distance in memory (in the number of elements) between adjacent elements in dimension k.

An array with a specified step is a subtype of the AbstractArray array, whose records are stored in memory with fixed steps. Provided that the array element type is compatible with BLAS, an array with a given step can use BLAS and LAPACK routines for more efficient linear algebra procedures. A standard example of a user-defined array with a specified step is an array that encloses a standard array Array in an additional structure.

Warning. Do not implement these methods if the underlying storage does not have the specified steps, as this may lead to incorrect results or segmentation errors.

Below are examples of array types with and without specified steps.

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], :)   # без заданного шага, так как расстояние между строками не является фиксированным.

Setting up the broadcast

Implemented methods Short description

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

Conducting an 'SrcType` type broadcast

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

Allocates an output container

Optional methods

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

Priority rules for mixing styles

Base.axes(x)

Declares the indexes of x according to the method axes(x).

Base.broadcastable(x)

Converts x into an object with `axes' and supports indexing

Bypassing default mechanisms

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

Custom implementation of broadcast

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

Custom implementation of broadcast!`with the `DestStyle specialization

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

Custom implementation of broadcast!`with the `DestType specialization

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

Overrides the default deferred behavior in a join expression.

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

Redefines calculations of deferred translation axes

Broadcast is activated explicitly by calling broadcast or broadcast! or implicitly by using point operations such as A .+ b or f.(x, y). Any object that has axes and indexing-enabled, can participate as an argument in the translation. By default, the result is stored in an Array. There are three main ways for this basic platform to be extensible:

  • Providing translation support for all arguments

  • Selecting the appropriate output array for a given set of arguments

  • Choosing an effective implementation for a given set of arguments

axes and indexing are not supported by all types, but many can be enabled in translation. Function Base.broadcastable is called for each broadcast argument, which allows you to return something else that supports axes and indexing. By default, this is the identity function for all AbstractArray and Number — they already support axes and indexing.

If the type is supposed to act as a "0-dimensional scalar" (a separate object), and not a container for translation, then the following method must be defined:

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

which returns an argument enclosed in a 0-dimensional container Ref. For example, such a wrapper method is defined for the types themselves, functions, and special single objects such as missing and nothing, and dates.

Custom array-like types can specialize Base.broadcastable to define their form, but they must follow the convention that collect(Base.broadcastable(x)) == collect(x). A significant exception is `AbstractString'. Strings have a special case to act as scalars for translation purposes, although they are iterable collections of their characters (for more information, see Lines).

The next two steps (selection of the output array and implementation) depend on defining a single solution for a given set of arguments. A broadcast should take all the different types of its arguments and reduce them to a single output array and a single implementation. In broadcasting, this single solution is called "style". Each broadcast object has its own preferred style, and to combine these styles into a single solution - the "destination style" - a system similar to promotion is used.

Broadcast Styles

Base.BroadcastStyle is an abstract type that all broadcast styles are derived from. When used as a function, it has two possible forms: unary (single-argument) and binary. The unary option means that you intend to implement a certain translation behavior and/or output type and do not want to use the default fallback type. Broadcast.DefaultArrayStyle.

To override these default settings, you can define a custom BroadcastStyle type for the object.:

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

In some cases, it is more rational not to define MyStyle, then it will be possible to use one of the common translation wrappers.:

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}() can be used for arbitrary types.

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}() is the preferred option if MyType is `AbstractArray'.

  • For AbstractArrays that only support a certain dimension, create a subtype of Broadcast.AbstractArrayStyle{N} (see below).

When multiple arguments are used in a translation operation, the styles of the individual arguments are combined to define a single `DestStyle' that controls the type of the output container. For more information, see below.

Selecting the appropriate output array

The broadcast style is calculated for each broadcast operation to allow for dispatching and specialization. The actual allocation of the result array is handled using similar, where the translated object is used as the first argument.

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

The backup definition looks like this.

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

However, if necessary, you can specialize any or all of these arguments. The last argument bc is a deferred representation of a (potentially combined) broadcast operation, a Broadcasted object. For these purposes, the most important shell fields are f and args, describing the function and the argument list, respectively. Note that the argument list can and often does contain other Broadcast nested wrappers.

For a complete example: let’s say you’ve created an ArrayAndChar type that stores an array and a single character.:

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, "'")
# вывод

It may be necessary for the char 'metadata' to be saved during the broadcast. First, it is determined

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

This means that the corresponding similar method should also be defined.:

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)

The following behavior follows from these definitions.

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

Extending translation with custom implementations

In general, the translation operation is represented by a deferred Broadcasted container, which stores the applied function along with the arguments. These arguments can themselves be more deeply nested Broadcasted containers, forming a large expression tree for evaluation. The Broadcast nested container tree is directly formed using an implicit dot syntax. For example, 5 .+ 2.x is temporarily represented by Broadcasted(+, 5, Broadcasted(, 2, x)). This action is invisible to users, as it is immediately implemented through the copy call, but it is this container that is the basis for the extensibility of translation for authors of custom types. Then the built-in translation mechanism will determine the type and size of the result based on the arguments, select it, and then copy the implementation of the Broadcast object into it using the default copyto!' method.(::AbstractArray, ::Broadcasted). Built-in backup methods broadcast and broadcast! similarly, a temporary representation of the Broadcast operation is formed to follow the same code execution path. This allows custom array implementations to provide their own specialization copyto! to set up and optimize the broadcast. Again, this moment is determined by the calculated broadcast style. This part of the operation is so important that it is stored as the first parameter of the Broadcast type, which allows for dispatching and specialization.

For some types, the mechanisms for "combining" operations at nested translation levels are not available or can be performed more efficiently in stages. In such cases, it may be necessary to evaluate the expression x' .* (x .+ 1), as if it were written as broadcast(, x, broadcast(+, x, 1)), where the internal operation is calculated before the external one is performed. This type of uncomplicated operation is directly supported indirectly. Instead of directly creating the Broadcast objects, Julia reduces the level of the combined expression x. (x .+ 1) before Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1)). Now, by default, broadcasted simply calls the Broadcasted constructor to create a deferred representation of the combined expression tree, but it can be overridden for a specific combination of function and arguments.

For example, the built-in AbstractRange objects use this mechanism to optimize parts of translated expressions that can be immediately evaluated purely in terms of start, step, and length (or stop) instead of calculating each individual element. Like all other mechanisms, broadcasted also computes and represents a combined translation style of its arguments, so instead of specializing +broadcasted(f, args…​)+`can be specialized+broadcast(::DestStyle, f, args…​)+` for any combination of style, function, and arguments.

For example, the following definition supports negation of ranges.

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

On-site broadcast expansion

On-site translation can be supported by defining the appropriate copyto!' method.(dest, bc::Broadcasted). Since it may be necessary to specialize either dest or a specific subtype of bc, the following convention is recommended to avoid ambiguity between packages.

To specialize a specific `DestStyle' style, define a method for

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

If necessary, you can also use this form to specialize the dest type.

If you need to specialize the target type DestType instead without specializing DestStyle, you should define a method with the following signature.

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

For this, a backup implementation of copyto! is used, which converts the shell to Broadcasted{Nothing}. Therefore, the specialization of DestType has a lower priority than the methods specializing in `DestStyle'.

Similarly, you can completely redefine the broadcast out of place using the copy(::Broadcasted).

Working with Broadcast objects

To implement a method such as copy or copyto!, you need to use the Broadcast wrapper to calculate each element. There are two main ways to do this.

  • Broadcast.flatten recalculates a potentially nested operation into a single function and a flat list of arguments. You are personally responsible for implementing the rules of the translation form, but this can only be useful in limited situations.

  • Iterating the CartesianIndices object of the axes(::Broadcast) method and using indexing with the resulting CartesianIndex object to calculate the result.

Writing Binary translation rules

The priority rules are determined by calls to the binary BroadcastStyle:

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

where Style12 is the BroadcastStyle to be selected for outputs with arguments Style1 and `Style2'. Examples:

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

indicates that Tuple has priority over zero-dimensional arrays (the output container will be a tuple). It is worth noting that it is not necessary (and should not be) define both argument orders for this call. It is enough to define one regardless of the order in which the user provides the arguments.

For the AbstractArray types, the definition of BroadcastStyle replaces the choice of a backup option. Broadcast.DefaultArrayStyle. 'DefaultArrayStyle` and the abstract supertype `AbstractArrayStyle' store dimension as a type parameter to support specialized array types with fixed dimension requirements.

'DefaultArrayStyle` "loses" to any other AbstractArrayStyle that has been defined by the following methods.

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)))

You don’t need to write binary BroadcastStyle rules, unless you want to set priority for two or more types that are not DefaultArrayStyle.

If the array type requires a fixed dimension, the AbstractArrayStyle subtype should be used. For example, the sparse array code has the following definitions.

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

Whenever the subtype is AbstractArrayStyle', it is necessary to define rules for the combination of dimensions by creating a constructor for the style that takes the argument `Val(N). For example:

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

These rules indicate that combining SparseVecStyle with null or one-dimensional arrays produces another SparseVecStyle, combining with a two-dimensional array produces SparseMatStyle, and everything with a higher dimension returns to a dense structure of arbitrary dimension. These rules allow the translation to preserve a sparse representation for operations that result in one- or two-dimensional output data, but which produce an Array for any other dimension.

Instance Properties

Implemented methods Default definition Short description

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

fieldnames(typeof(x))

Returns a tuple of properties (x.property) of the object x. If private=true, the names of the properties that should remain private are also returned.

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

getfield(x, s)

Returns the property s of object x. x.s calls getproperty(x, :s).

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

setfield!(x, s, v)

Assigns the value v to the property s of the object x. x.s = v calls setproperty!(x, :s, v). The value v should be returned.

Sometimes it is desirable to change the way the end user interacts with the object fields. Instead of providing direct access to type fields, you can introduce an additional level of abstraction between the user and the code by overloading object.field. Properties are how the user sees the object, and fields are what the object actually represents.

By default, the properties and fields match. However, this behavior can be changed. For example, take the representation of a point on a plane in https://en.wikipedia.org/wiki/Polar_coordinate_system [polar coordinates]:

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

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

As described in the table above, access via the dot notation p.r is equivalent to calling getproperty(p, :r), which by default is equivalent to 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)

However, we may need users not to know that the coordinates in Point are stored as r and ϕ (fields), but instead interact with x and y (properties). You can define the methods in the first column by adding new functionality.:

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

It is important to use the getfield and setfield calls inside getproperty and setproperty! instead of the dot syntax, since dot syntax will make functions recursive, which can lead to problems with type inference. Now you can experience the new functionality.:

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

julia> p.x
4.949747468305833

julia> p.y = 4.0
4.0

julia> p.r
6.363961030678928

In conclusion, it is worth noting that in Julia, instance properties are rarely added in this way. As a rule, there must be a good reason for this.

Rounding up

Implemented methods Default definition Short description

round(x::ObjType, r::RoundingMode)

no

Rounds up the x and returns the result. If possible, the round method should return an object of the same type as `x'.

round(T::Type, x::ObjType, r::RoundingMode)

convert(T, round(x, r))

Rounds up the x, returning the result as a `T'.

To support rounding in a new type, it is usually sufficient to define one method round(x::ObjType, r::RoundingMode). The transmitted rounding mode determines in which direction the value should be rounded. The most commonly used rounding modes are RoundNearest, RoundToZero, RoundDown and RoundUp, as they are used in definitions of a single argument, round, method, and trunk, floor and `ceil', respectively.

In some cases, it is possible to define a three-argument round method that is more accurate or productive than a two-argument method followed by a transformation. In this case, you can define a three-argument method in addition to a two-argument one. If it is impossible to represent the rounded result as a type T object, the method with three arguments should raise the error `InexactError'.

For example, if there is an Interval type that represents a range of possible values, like https://github.com/JuliaPhysics/Measurements.jl , you can define rounding for this type as follows

julia> struct Interval{T}
           min::T
           max::T
       end

julia> Base.round(x::Interval, r::RoundingMode) = Interval(round(x.min, r), round(x.max, r))

julia> x = Interval(1.7, 2.2)
Interval{Float64}(1.7, 2.2)

julia> round(x)
Interval{Float64}(2.0, 2.0)

julia> floor(x)
Interval{Float64}(1.0, 2.0)

julia> ceil(x)
Interval{Float64}(2.0, 3.0)

julia> trunc(x)
Interval{Float64}(1.0, 2.0)