Movatterモバイル変換


[0]ホーム

URL:


GitHub

Interfaces

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.

Iteration

There are two methods that are always required:

Required methodBrief 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().

MethodWhen should this method be defined?Default definitionBrief description
Base.IteratorSize(IterType)If default is not appropriateBase.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 appropriateBase.HasEltype()EitherBase.EltypeUnknown() orBase.HasEltype() as appropriate
eltype(IterType)If default is not appropriateAnyThe type of the first entry of the tuple returned byiterate()
Base.isdone(iter, [state])Must be defined if iterator is statefulmissingFast-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

Indexing

Methods to implementBrief 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 theith 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 ofInts, 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.

Abstract Arrays

Methods to implementBrief 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 methodsDefault definitionBrief 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 scalargetindexMultidimensional and nonscalar indexing
setindex!(A, X, I...)defined in terms of scalarsetindex!Multidimensional and nonscalar indexed assignment
iteratedefined in terms of scalargetindexIteration
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 indicesDefault definitionBrief 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.

Strided Arrays

Methods to implementBrief 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 methodsDefault definitionBrief 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.

Customizing broadcasting

Methods to implementBrief 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 allAbstractArrays andNumbers — 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".

Broadcast Styles

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:

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.

Selecting an appropriate output array

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

Extending broadcast with custom implementations

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

Extending in-place broadcasting

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.

Working withBroadcasted objects

In 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:

Writing binary broadcasting rules

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.

Instance Properties

Methods to implementDefault definitionBrief 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.

Rounding

Methods to implementDefault definitionBrief description
round(x::ObjType, r::RoundingMode)noneRoundx 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.


[8]ページ先頭

©2009-2025 Movatter.jp