A lot of the power and extensibility in Julia comes from a collection of informal interfaces. By extending a few specific methods to work for a custom type, objects of that type not only receive those functionalities, but they are also able to be used in other methods that are written to generically build upon those behaviors.
There are two methods that are always required:
Required method | Brief description |
---|---|
iterate(iter) | Returns either a tuple of the first item and initial state ornothing if empty |
iterate(iter, state) | Returns either a tuple of the next item and next state ornothing if no items remain |
There are several more methods that should be defined in some circumstances. Please note that you should always define at least one ofBase.IteratorSize(IterType)
andlength(iter)
because the default definition ofBase.IteratorSize(IterType)
isBase.HasLength()
.
Method | When should this method be defined? | Default definition | Brief description |
---|---|---|---|
Base.IteratorSize(IterType) | If default is not appropriate | Base.HasLength() | One ofBase.HasLength() ,Base.HasShape{N}() ,Base.IsInfinite() , orBase.SizeUnknown() as appropriate |
length(iter) | IfBase.IteratorSize() returnsBase.HasLength() orBase.HasShape{N}() | (undefined) | The number of items, if known |
size(iter, [dim]) | IfBase.IteratorSize() returnsBase.HasShape{N}() | (undefined) | The number of items in each dimension, if known |
Base.IteratorEltype(IterType) | If default is not appropriate | Base.HasEltype() | EitherBase.EltypeUnknown() orBase.HasEltype() as appropriate |
eltype(IterType) | If default is not appropriate | Any | The type of the first entry of the tuple returned byiterate() |
Base.isdone(iter, [state]) | Must be defined if iterator is stateful | missing | Fast-path hint for iterator completion. If not defined for a stateful iterator then functions that check for done-ness, likeisempty() andzip() , may mutate the iterator and cause buggy behaviour! |
Sequential iteration is implemented by theiterate
function. Instead of mutating objects as they are iterated over, Julia iterators may keep track of the iteration state externally from the object. The return value from iterate is always either a tuple of a value and a state, ornothing
if no elements remain. The state object will be passed back to the iterate function on the next iteration and is generally considered an implementation detail private to the iterable object.
Any object that defines this function is iterable and can be used in themany functions that rely upon iteration. It can also be used directly in afor
loop since the syntax:
for item in iter # or "for item = iter" # bodyend
is translated into:
next = iterate(iter)while next !== nothing (item, state) = next # body next = iterate(iter, state)end
A simple example is an iterable sequence of square numbers with a defined length:
julia> struct Squares count::Int endjulia> Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)
With onlyiterate
definition, theSquares
type is already pretty powerful. We can iterate over all the elements:
julia> for item in Squares(7) println(item) end14916253649
We can use many of the builtin methods that work with iterables, likein
orsum
:
julia> 25 in Squares(10)truejulia> sum(Squares(100))338350
There are a few more methods we can extend to give Julia more information about this iterable collection. We know that the elements in aSquares
sequence will always beInt
. By extending theeltype
method, we can give that information to Julia and help it make more specialized code in the more complicated methods. We also know the number of elements in our sequence, so we can extendlength
, too:
julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the typejulia> Base.length(S::Squares) = S.count
Now, when we ask Julia tocollect
all the elements into an array it can preallocate aVector{Int}
of the right size instead of naivelypush!
ing each element into aVector{Any}
:
julia> collect(Squares(4))4-element Vector{Int64}: 1 4 9 16
While we can rely upon generic implementations, we can also extend specific methods where we know there is a simpler algorithm. For example, there's a formula to compute the sum of squares, so we can override the generic iterative version with a more performant solution:
julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)julia> sum(Squares(1803))1955361914
This is a very common pattern throughout Julia Base: a small set of required methods define an informal interface that enable many fancier behaviors. In some cases, types will want to additionally specialize those extra behaviors when they know a more efficient algorithm can be used in their specific case.
It is also often useful to allow iteration over a collection inreverse order by iterating overIterators.reverse(iterator)
. To actually support reverse-order iteration, however, an iterator typeT
needs to implementiterate
forIterators.Reverse{T}
. (Givenr::Iterators.Reverse{T}
, the underling iterator of typeT
isr.itr
.) In ourSquares
example, we would implementIterators.Reverse{Squares}
methods:
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
Methods to implement | Brief description |
---|---|
getindex(X, i) | X[i] , indexed access, non-scalari should allocate a copy |
setindex!(X, v, i) | X[i] = v , indexed assignment |
firstindex(X) | The first index, used inX[begin] |
lastindex(X) | The last index, used inX[end] |
For theSquares
iterable above, we can easily compute thei
th element of the sequence by squaring it. We can expose this as an indexing expressionS[i]
. To opt into this behavior,Squares
simply needs to definegetindex
:
julia> function Base.getindex(S::Squares, i::Int) 1 <= i <= S.count || throw(BoundsError(S, i)) return i*i endjulia> Squares(100)[23]529
Additionally, to support the syntaxS[begin]
andS[end]
, we must definefirstindex
andlastindex
to specify the first and last valid indices, respectively:
julia> Base.firstindex(S::Squares) = 1julia> Base.lastindex(S::Squares) = length(S)julia> Squares(23)[end]529
For multi-dimensionalbegin
/end
indexing as ina[3, begin, 7]
, for example, you should definefirstindex(a, dim)
andlastindex(a, dim)
(which default to callingfirst
andlast
onaxes(a, dim)
, respectively).
Note, though, that the aboveonly definesgetindex
with one integer index. Indexing with anything other than anInt
will throw aMethodError
saying that there was no matching method. In order to support indexing with ranges or vectors ofInt
s, separate methods must be written:
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
While this is starting to support more of theindexing operations supported by some of the builtin types, there's still quite a number of behaviors missing. ThisSquares
sequence is starting to look more and more like a vector as we've added behaviors to it. Instead of defining all these behaviors ourselves, we can officially define it as a subtype of anAbstractArray
.
Methods to implement | Brief description | |
---|---|---|
size(A) | Returns a tuple containing the dimensions ofA | |
getindex(A, i::Int) | (ifIndexLinear ) Linear scalar indexing | |
getindex(A, I::Vararg{Int, N}) | (ifIndexCartesian , whereN = ndims(A) ) N-dimensional scalar indexing | |
Optional methods | Default definition | Brief description |
IndexStyle(::Type) | IndexCartesian() | Returns eitherIndexLinear() orIndexCartesian() . See the description below. |
setindex!(A, v, i::Int) | (ifIndexLinear ) Scalar indexed assignment | |
setindex!(A, v, I::Vararg{Int, N}) | (ifIndexCartesian , whereN = ndims(A) ) N-dimensional scalar indexed assignment | |
getindex(A, I...) | defined in terms of scalargetindex | Multidimensional and nonscalar indexing |
setindex!(A, X, I...) | defined in terms of scalarsetindex! | Multidimensional and nonscalar indexed assignment |
iterate | defined in terms of scalargetindex | Iteration |
length(A) | prod(size(A)) | Number of elements |
similar(A) | similar(A, eltype(A), size(A)) | Return a mutable array with the same shape and element type |
similar(A, ::Type{S}) | similar(A, S, size(A)) | Return a mutable array with the same shape and the specified element type |
similar(A, dims::Dims) | similar(A, eltype(A), dims) | Return a mutable array with the same element type and sizedims |
similar(A, ::Type{S}, dims::Dims) | Array{S}(undef, dims) | Return a mutable array with the specified element type and size |
Non-traditional indices | Default definition | Brief description |
axes(A) | map(OneTo, size(A)) | Return a tuple ofAbstractUnitRange{<:Integer} of valid indices. The axes should be their own axes, that isaxes.(axes(A),1) == axes(A) should be satisfied. |
similar(A, ::Type{S}, inds) | similar(A, S, Base.to_shape(inds)) | Return a mutable array with the specified indicesinds (see below) |
similar(T::Union{Type,Function}, inds) | T(Base.to_shape(inds)) | Return an array similar toT with the specified indicesinds (see below) |
If a type is defined as a subtype ofAbstractArray
, it inherits a very large set of rich behaviors including iteration and multidimensional indexing built on top of single-element access. See thearrays manual page and theJulia Base section for more supported methods.
A key part in defining anAbstractArray
subtype isIndexStyle
. Since indexing is such an important part of an array and often occurs in hot loops, it's important to make both indexing and indexed assignment as efficient as possible. Array data structures are typically defined in one of two ways: either it most efficiently accesses its elements using just one index (linear indexing) or it intrinsically accesses the elements with indices specified for every dimension. These two modalities are identified by Julia asIndexLinear()
andIndexCartesian()
. Converting a linear index to multiple indexing subscripts is typically very expensive, so this provides a traits-based mechanism to enable efficient generic code for all array types.
This distinction determines which scalar indexing methods the type must define.IndexLinear()
arrays are simple: just definegetindex(A::ArrayType, i::Int)
. When the array is subsequently indexed with a multidimensional set of indices, the fallbackgetindex(A::AbstractArray, I...)
efficiently converts the indices into one linear index and then calls the above method.IndexCartesian()
arrays, on the other hand, require methods to be defined for each supported dimensionality withndims(A)
Int
indices. For example,SparseMatrixCSC
from theSparseArrays
standard library module, only supports two dimensions, so it just definesgetindex(A::SparseMatrixCSC, i::Int, j::Int)
. The same holds forsetindex!
.
Returning to the sequence of squares from above, we could instead define it as a subtype of anAbstractArray{Int, 1}
:
julia> struct SquaresVector <: AbstractArray{Int, 1} count::Int endjulia> Base.size(S::SquaresVector) = (S.count,)julia> Base.IndexStyle(::Type{<:SquaresVector}) = IndexLinear()julia> Base.getindex(S::SquaresVector, i::Int) = i*i
Note that it's very important to specify the two parameters of theAbstractArray
; the first defines theeltype
, and the second defines thendims
. That supertype and those three methods are all it takes forSquaresVector
to be an iterable, indexable, and completely functional array:
julia> s = SquaresVector(4)4-element SquaresVector: 1 4 9 16julia> s[s .> 8]2-element Vector{Int64}: 9 16julia> s + s4-element Vector{Int64}: 2 8 18 32julia> sin.(s)4-element Vector{Float64}: 0.8414709848078965 -0.7568024953079282 0.4121184852417566 -0.2879033166650653
As a more complicated example, let's define our own toy N-dimensional sparse-like array type built on top ofDict
:
julia> struct SparseArray{T,N} <: AbstractArray{T,N} data::Dict{NTuple{N,Int}, T} dims::NTuple{N,Int} endjulia> 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.dimsjulia> 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)
Notice that this is anIndexCartesian
array, so we must manually definegetindex
andsetindex!
at the dimensionality of the array. Unlike theSquaresVector
, we are able to definesetindex!
, and so we can mutate 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.0julia> 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.0julia> A[:] = 1:length(A); A3×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 anAbstractArray
can itself be an array (for instance when indexing by anAbstractRange
). TheAbstractArray
fallback methods usesimilar
to allocate anArray
of the appropriate size and element type, which is filled in using the basic indexing method described above. However, when implementing an array wrapper you often want the result to be wrapped as well:
julia> A[1:2,:]2×3 SparseArray{Float64, 2}: 1.0 4.0 7.0 2.0 5.0 8.0
In this example it is accomplished by definingBase.similar(A::SparseArray, ::Type{T}, dims::Dims) where T
to create the appropriate wrapped array. (Note that whilesimilar
supports 1- and 2-argument forms, in most case you only need to specialize the 3-argument form.) For this to work it's important thatSparseArray
is mutable (supportssetindex!
). Definingsimilar
,getindex
andsetindex!
forSparseArray
also makes it possible tocopy
the array:
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 iterable and indexable methods from above, these types can also interact with each other and use most of the methods defined in Julia Base forAbstractArrays
:
julia> A[SquaresVector(3)]3-element SparseArray{Float64, 1}: 1.0 4.0 9.0julia> sum(A)45.0
If you are defining an array type that allows non-traditional indexing (indices that start at something other than 1), you should specializeaxes
. You should also specializesimilar
so that thedims
argument (ordinarily aDims
size-tuple) can acceptAbstractUnitRange
objects, perhaps range-typesInd
of your own design. For more information, seeArrays with custom indices.
Methods to implement | Brief description | |
---|---|---|
strides(A) | Return the distance in memory (in number of elements) between adjacent elements in each dimension as a tuple. IfA is anAbstractArray{T,0} , this should return an empty tuple. | |
Base.unsafe_convert(::Type{Ptr{T}}, A) | Return the native address of an array. | |
Base.elsize(::Type{<:A}) | Return the stride between consecutive elements in the array. | |
Optional methods | Default definition | Brief description |
stride(A, i::Int) | strides(A)[i] | Return the distance in memory (in number of elements) between adjacent elements in dimension k. |
A strided array is a subtype ofAbstractArray
whose entries are stored in memory with fixed strides. Provided the element type of the array is compatible with BLAS, a strided array can utilize BLAS and LAPACK routines for more efficient linear algebra routines. A typical example of a user-defined strided array is one that wraps a standardArray
with additional structure.
Warning: do not implement these methods if the underlying storage is not actually strided, as it may lead to incorrect results or segmentation faults.
Here are some examples to demonstrate which type of arrays are strided and which are not:
1:5 # not strided (there is no storage associated with this array.)Vector(1:5) # is strided with strides (1,)A = [1 5; 2 6; 3 7; 4 8] # is strided with strides (1,4)V = view(A, 1:2, :) # is strided with strides (1,4)V = view(A, 1:2:3, 1:2) # is strided with strides (2,4)V = view(A, [1,2,4], :) # is not strided, as the spacing between rows is not fixed.
Methods to implement | Brief description |
---|---|
Base.BroadcastStyle(::Type{SrcType}) = SrcStyle() | Broadcasting behavior ofSrcType |
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType}) | Allocation of output container |
Optional methods | |
Base.BroadcastStyle(::Style1, ::Style2) = Style12() | Precedence rules for mixing styles |
Base.axes(x) | Declaration of the indices ofx , as peraxes(x) . |
Base.broadcastable(x) | Convertx to an object that hasaxes and supports indexing |
Bypassing default machinery | |
Base.copy(bc::Broadcasted{DestStyle}) | Custom implementation ofbroadcast |
Base.copyto!(dest, bc::Broadcasted{DestStyle}) | Custom implementation ofbroadcast! , specializing onDestStyle |
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing}) | Custom implementation ofbroadcast! , specializing onDestType |
Base.Broadcast.broadcasted(f, args...) | Override the default lazy behavior within a fused expression |
Base.Broadcast.instantiate(bc::Broadcasted{DestStyle}) | Override the computation of the lazy broadcast's axes |
Broadcasting is triggered by an explicit call tobroadcast
orbroadcast!
, or implicitly by "dot" operations likeA .+ b
orf.(x, y)
. Any object that hasaxes
and supports indexing can participate as an argument in broadcasting, and by default the result is stored in anArray
. This basic framework is extensible in three major ways:
Not all types supportaxes
and indexing, but many are convenient to allow in broadcast. TheBase.broadcastable
function is called on each argument to broadcast, allowing it to return something different that supportsaxes
and indexing. By default, this is the identity function for allAbstractArray
s andNumber
s — they already supportaxes
and indexing.
If a type is intended to act like a "0-dimensional scalar" (a single object) rather than as a container for broadcasting, then the following method should be defined:
Base.broadcastable(o::MyType) = Ref(o)
that returns the argument wrapped in a 0-dimensionalRef
container. For example, such a wrapper method is defined for types themselves, functions, special singletons likemissing
andnothing
, and dates.
Custom array-like types can specializeBase.broadcastable
to define their shape, but they should follow the convention thatcollect(Base.broadcastable(x)) == collect(x)
. A notable exception isAbstractString
; strings are special-cased to behave as scalars for the purposes of broadcast even though they are iterable collections of their characters (seeStrings for more).
The next two steps (selecting the output array and implementation) are dependent upon determining a single answer for a given set of arguments. Broadcast must take all the varied types of its arguments and collapse them down to just one output array and one implementation. Broadcast calls this single answer a "style". Every broadcastable object each has its own preferred style, and a promotion-like system is used to combine these styles into a single answer — the "destination style".
Base.BroadcastStyle
is the abstract type from which all broadcast styles are derived. When used as a function it has two possible forms, unary (single-argument) and binary. The unary variant states that you intend to implement specific broadcasting behavior and/or output type, and do not wish to rely on the default fallbackBroadcast.DefaultArrayStyle
.
To override these defaults, you can define a customBroadcastStyle
for your object:
struct MyStyle <: Broadcast.BroadcastStyle endBase.BroadcastStyle(::Type{<:MyType}) = MyStyle()
In some cases it might be convenient not to have to defineMyStyle
, in which case you can leverage one of the general broadcast wrappers:
Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}()
can be used for arbitrary types.Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}()
is preferred ifMyType
is anAbstractArray
.AbstractArrays
that only support a certain dimensionality, create a subtype ofBroadcast.AbstractArrayStyle{N}
(see below).When your broadcast operation involves several arguments, individual argument styles get combined to determine a singleDestStyle
that controls the type of the output container. For more details, seebelow.
The broadcast style is computed for every broadcasting operation to allow for dispatch and specialization. The actual allocation of the result array is handled bysimilar
, using the Broadcasted object as its first argument.
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})
The fallback definition is
similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} = similar(Array{ElType}, axes(bc))
However, if needed you can specialize on any or all of these arguments. The final argumentbc
is a lazy representation of a (potentially fused) broadcast operation, aBroadcasted
object. For these purposes, the most important fields of the wrapper aref
andargs
, describing the function and argument list, respectively. Note that the argument list can — and often does — include other nestedBroadcasted
wrappers.
For a complete example, let's say you have created a type,ArrayAndChar
, that stores an array and a single character:
struct ArrayAndChar{T,N} <: AbstractArray{T,N} data::Array{T,N} char::CharendBase.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...] = valBase.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'")
You might want broadcasting to preserve thechar
"metadata". First we define
Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()
This means we must also define a correspondingsimilar
method:
function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType # Scan the inputs for the ArrayAndChar: A = find_aac(bc) # Use the char field of A to create the output 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) = xfind_aac(::Tuple{}) = nothingfind_aac(a::ArrayAndChar, rest) = afind_aac(::Any, rest) = find_aac(rest)
From these definitions, one obtains the following behavior:
julia> a = ArrayAndChar([1 2; 3 4], 'x')2×2 ArrayAndChar{Int64, 2} with char 'x': 1 2 3 4julia> a .+ 12×2 ArrayAndChar{Int64, 2} with char 'x': 2 3 4 5julia> a .+ [5,10]2×2 ArrayAndChar{Int64, 2} with char 'x': 6 7 13 14
In general, a broadcast operation is represented by a lazyBroadcasted
container that holds onto the function to be applied alongside its arguments. Those arguments may themselves be more nestedBroadcasted
containers, forming a large expression tree to be evaluated. A nested tree ofBroadcasted
containers is directly constructed by the implicit dot syntax;5 .+ 2.*x
is transiently represented byBroadcasted(+, 5, Broadcasted(*, 2, x))
, for example. This is invisible to users as it is immediately realized through a call tocopy
, but it is this container that provides the basis for broadcast's extensibility for authors of custom types. The built-in broadcast machinery will then determine the result type and size based upon the arguments, allocate it, and then finally copy the realization of theBroadcasted
object into it with a defaultcopyto!(::AbstractArray, ::Broadcasted)
method. The built-in fallbackbroadcast
andbroadcast!
methods similarly construct a transientBroadcasted
representation of the operation so they can follow the same codepath. This allows custom array implementations to provide their owncopyto!
specialization to customize and optimize broadcasting. This is again determined by the computed broadcast style. This is such an important part of the operation that it is stored as the first type parameter of theBroadcasted
type, allowing for dispatch and specialization.
For some types, the machinery to "fuse" operations across nested levels of broadcasting is not available or could be done more efficiently incrementally. In such cases, you may need or want to evaluatex .* (x .+ 1)
as if it had been writtenbroadcast(*, x, broadcast(+, x, 1))
, where the inner operation is evaluated before tackling the outer operation. This sort of eager operation is directly supported by a bit of indirection; instead of directly constructingBroadcasted
objects, Julia lowers the fused expressionx .* (x .+ 1)
toBroadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1))
. Now, by default,broadcasted
just calls theBroadcasted
constructor to create the lazy representation of the fused expression tree, but you can choose to override it for a particular combination of function and arguments.
As an example, the builtinAbstractRange
objects use this machinery to optimize pieces of broadcasted expressions that can be eagerly evaluated purely in terms of the start, step, and length (or stop) instead of computing every single element. Just like all the other machinery,broadcasted
also computes and exposes the combined broadcast style of its arguments, so instead of specializing onbroadcasted(f, args...)
, you can specialize onbroadcasted(::DestStyle, f, args...)
for any combination of style, function, and arguments.
For example, the following definition supports the negation of ranges:
broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))
In-place broadcasting can be supported by defining the appropriatecopyto!(dest, bc::Broadcasted)
method. Because you might want to specialize either ondest
or the specific subtype ofbc
, to avoid ambiguities between packages we recommend the following convention.
If you wish to specialize on a particular styleDestStyle
, define a method for
copyto!(dest, bc::Broadcasted{DestStyle})
Optionally, with this form you can also specialize on the type ofdest
.
If instead you want to specialize on the destination typeDestType
without specializing onDestStyle
, then you should define a method with the following signature:
copyto!(dest::DestType, bc::Broadcasted{Nothing})
This leverages a fallback implementation ofcopyto!
that converts the wrapper into aBroadcasted{Nothing}
. Consequently, specializing onDestType
has lower precedence than methods that specialize onDestStyle
.
Similarly, you can completely override out-of-place broadcasting with acopy(::Broadcasted)
method.
Broadcasted
objectsIn order to implement such acopy
orcopyto!
, method, of course, you must work with theBroadcasted
wrapper to compute each element. There are two main ways of doing so:
Broadcast.flatten
recomputes the potentially nested operation into a single function and flat list of arguments. You are responsible for implementing the broadcasting shape rules yourself, but this may be helpful in limited situations.CartesianIndices
of theaxes(::Broadcasted)
and using indexing with the resultingCartesianIndex
object to compute the result.The precedence rules are defined by binaryBroadcastStyle
calls:
Base.BroadcastStyle(::Style1, ::Style2) = Style12()
whereStyle12
is theBroadcastStyle
you want to choose for outputs involving arguments ofStyle1
andStyle2
. For example,
Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()
indicates thatTuple
"wins" over zero-dimensional arrays (the output container will be a tuple). It is worth noting that you do not need to (and should not) define both argument orders of this call; defining one is sufficient no matter what order the user supplies the arguments in.
ForAbstractArray
types, defining aBroadcastStyle
supersedes the fallback choice,Broadcast.DefaultArrayStyle
.DefaultArrayStyle
and the abstract supertype,AbstractArrayStyle
, store the dimensionality as a type parameter to support specialized array types that have fixed dimensionality requirements.
DefaultArrayStyle
"loses" to any otherAbstractArrayStyle
that has been defined because of the following methods:
BroadcastStyle(a::AbstractArrayStyle{Any}, ::DefaultArrayStyle) = aBroadcastStyle(a::AbstractArrayStyle{N}, ::DefaultArrayStyle{N}) where N = aBroadcastStyle(a::AbstractArrayStyle{M}, ::DefaultArrayStyle{N}) where {M,N} = typeof(a)(Val(max(M, N)))
You do not need to write binaryBroadcastStyle
rules unless you want to establish precedence for two or more non-DefaultArrayStyle
types.
If your array type does have fixed dimensionality requirements, then you should subtypeAbstractArrayStyle
. For example, the sparse array code has the following definitions:
struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} endstruct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} endBase.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()
Whenever you subtypeAbstractArrayStyle
, you also need to define rules for combining dimensionalities, by creating a constructor for your style that takes aVal(N)
argument. 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 the combination of aSparseVecStyle
with 0- or 1-dimensional arrays yields anotherSparseVecStyle
, that its combination with a 2-dimensional array yields aSparseMatStyle
, and anything of higher dimensionality falls back to the dense arbitrary-dimensional framework. These rules allow broadcasting to keep the sparse representation for operations that result in one or two dimensional outputs, but produce anArray
for any other dimensionality.
Methods to implement | Default definition | Brief description |
---|---|---|
propertynames(x::ObjType, private::Bool=false) | fieldnames(typeof(x)) | Return a tuple of the properties (x.property ) of an objectx . Ifprivate=true , also return property names intended to be kept as private |
getproperty(x::ObjType, s::Symbol) | getfield(x, s) | Return propertys ofx .x.s callsgetproperty(x, :s) . |
setproperty!(x::ObjType, s::Symbol, v) | setfield!(x, s, v) | Set propertys ofx tov .x.s = v callssetproperty!(x, :s, v) . Should returnv . |
Sometimes, it is desirable to change how the end-user interacts with the fields of an object. Instead of granting direct access to type fields, an extra layer of abstraction between the user and the code can be provided by overloadingobject.field
. Properties are what the usersees of the object, fields what the objectactually is.
By default, properties and fields are the same. However, this behavior can be changed. For example, take this representation of a point in a plane inpolar coordinates:
julia> mutable struct Point r::Float64 ϕ::Float64 endjulia> p = Point(7.0, pi/4)Point(7.0, 0.7853981633974483)
As described in the table above dot accessp.r
is the same asgetproperty(p, :r)
which is by default the same asgetfield(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 want users to be unaware thatPoint
stores the coordinates asr
andϕ
(fields), and instead interact withx
andy
(properties). The methods in the first column can be defined to add 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 # This allows accessing fields with p.r and p.ϕ return getfield(p, s) end endjulia> 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 # This allow modifying fields with p.r and p.ϕ return setfield!(p, s, f) end end
It is important thatgetfield
andsetfield
are used insidegetproperty
andsetproperty!
instead of the dot syntax, since the dot syntax would make the functions recursive which can lead to type inference issues. We can now try out the new functionality:
julia> propertynames(p)(:x, :y)julia> p.x4.949747468305833julia> p.y = 4.04.0julia> p.r6.363961030678928
Finally, it is worth noting that adding instance properties like this is quite rarely done in Julia and should in general only be done if there is a good reason for doing so.
Methods to implement | Default definition | Brief description |
---|---|---|
round(x::ObjType, r::RoundingMode) | none | Roundx and return the result. If possible, round should return an object of the same type asx |
round(T::Type, x::ObjType, r::RoundingMode) | convert(T, round(x, r)) | Roundx , returning the result as aT |
To support rounding on a new type it is typically sufficient to define the single methodround(x::ObjType, r::RoundingMode)
. The passed rounding mode determines in which direction the value should be rounded. The most commonly used rounding modes areRoundNearest
,RoundToZero
,RoundDown
, andRoundUp
, as these rounding modes are used in the definitions of the one argumentround
, method, andtrunc
,floor
, andceil
, respectively.
In some cases, it is possible to define a three-argumentround
method that is more accurate or performant than the two-argument method followed by conversion. In this case it is acceptable to define the three argument method in addition to the two argument method. If it is impossible to represent the rounded result as an object of the typeT
, then the three argument method should throw anInexactError
.
For example, if we have anInterval
type which represents a range of possible values similar to https://github.com/JuliaPhysics/Measurements.jl, we may define rounding on that type with the following
julia> struct Interval{T} min::T max::T endjulia> 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)
Settings
This document was generated withDocumenter.jl version 1.8.0 onWednesday 9 July 2025. Using Julia version 1.11.6.