Interfaces
Iteration
Two mandatory methods are always required.
Mandatory method | Short description |
---|---|
Returns either a tuple of the first element and the initial state, or |
|
|
Returns either a tuple of the next element and the next state, or |
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 |
---|---|---|---|
If the default value is not suitable |
|
One of the |
|
If |
(not definite) |
The number of elements, if known |
|
If |
(not definite) |
The number of elements in each dimension, if known |
|
If the default value is not suitable |
|
Or |
|
If the default value is not suitable |
|
The type of the first entry of the tuple returned by |
|
Must be determined if the iterator is monitoring the state |
|
Specifying a fast path to complete the iterator. If not defined for a stateful iterator, readiness-checking functions such as |
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
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 |
---|---|
|
|
|
|
|
The first index used in |
|
The last index used in |
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 | |
---|---|---|
|
Returns a tuple containing the dimensions |
|
|
(if |
|
|
(if |
|
Optional methods |
Default definition |
Short description |
|
|
Returns |
|
(if |
|
|
(if |
|
|
it is defined in terms of the scalar function |
|
|
it is defined in terms of the scalar function |
|
|
it is defined in terms of the scalar function |
Iteration |
|
|
Number of elements |
|
|
Returns a mutable array with the same shape and type of elements. |
|
|
Returns a mutable array with the same shape and the specified element type. |
|
|
Returns a mutable array with the same element type and_size dimensions |
|
|
Returns a mutable array with the specified element type and size |
Non-traditional indexes |
Default definition |
Short description |
|
|
Returns the tuple |
|
|
Returns a mutable array with the specified `inds' indexes (see below) |
|
|
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 | |
---|---|---|
|
Returns the distance in memory (in the number of elements) between adjacent elements in each dimension as a tuple. If |
|
|
Returns the array’s own address. |
|
|
Returns the step between consecutive elements in an array. |
|
Optional methods |
Default definition |
Short description |
|
|
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 |
---|---|
|
Conducting an 'SrcType` type broadcast |
|
Allocates an output container |
Optional methods |
|
|
Priority rules for mixing styles |
|
Declares the indexes of |
|
Converts |
Bypassing default mechanisms |
|
|
Custom implementation of |
|
Custom implementation of |
|
Custom implementation of |
|
Overrides the default deferred behavior in a join expression. |
|
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 ifMyType
is `AbstractArray'. -
For
AbstractArrays
that only support a certain dimension, create a subtype ofBroadcast.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 theaxes(::Broadcast)
method and using indexing with the resultingCartesianIndex
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 |
---|---|---|
|
|
Returns a tuple of properties ( |
|
|
Returns the property |
|
|
Assigns the value |
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 |
---|---|---|
|
no |
Rounds up the |
|
|
Rounds up the |
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)