Interfaces

줄리아의 많은 힘과 확장성은 비공식 인터페이스 모음에서 비롯됩니다. 특정 메서드를 사용자 정의 유형에 맞게 확장함으로써, 해당 유형의 객체는 이러한 기능을 받을 뿐만 아니라, 이러한 동작을 일반적으로 기반으로 작성된 다른 메서드에서도 사용될 수 있습니다.

Iteration

항상 필요한 두 가지 방법이 있습니다:

Required methodBrief description
iterate(iter)Returns either a tuple of the first item and initial state or nothing if empty
iterate(iter, state)Returns either a tuple of the next item and next state or nothing if no items remain

어떤 상황에서는 정의해야 할 몇 가지 추가 메서드가 있습니다. 항상 Base.IteratorSize(IterType)length(iter) 중 적어도 하나는 정의해야 한다는 점에 유의하세요. Base.IteratorSize(IterType)의 기본 정의는 Base.HasLength()입니다.

MethodWhen should this method be defined?Default definitionBrief description
Base.IteratorSize(IterType)If default is not appropriateBase.HasLength()One of Base.HasLength(), Base.HasShape{N}(), Base.IsInfinite(), or Base.SizeUnknown() as appropriate
length(iter)If Base.IteratorSize() returns Base.HasLength() or Base.HasShape{N}()(undefined)The number of items, if known
size(iter, [dim])If Base.IteratorSize() returns Base.HasShape{N}()(undefined)The number of items in each dimension, if known
Base.IteratorEltype(IterType)If default is not appropriateBase.HasEltype()Either Base.EltypeUnknown() or Base.HasEltype() as appropriate
eltype(IterType)If default is not appropriateAnyThe type of the first entry of the tuple returned by iterate()
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, like isempty() and zip(), may mutate the iterator and cause buggy behaviour!

순차 반복은 iterate 함수에 의해 구현됩니다. 반복되는 객체를 변형하는 대신, 줄리아 반복자는 객체 외부에서 반복 상태를 추적할 수 있습니다. iterate의 반환 값은 항상 값과 상태의 튜플이거나, 더 이상 요소가 남아 있지 않으면 nothing입니다. 상태 객체는 다음 반복에서 iterate 함수로 다시 전달되며, 일반적으로 반복 가능한 객체에 대해 비공식적인 구현 세부 사항으로 간주됩니다.

어떤 객체가 이 함수를 정의하면 반복 가능하며 many functions that rely upon iteration에서 사용할 수 있습니다. 또한 구문이 다음과 같기 때문에 for 루프에서 직접 사용할 수도 있습니다:

for item in iter   # or  "for item = iter"
    # body
end

is translated into:

next = iterate(iter)
while next !== nothing
    (item, state) = next
    # body
    next = iterate(iter, state)
end

정의된 길이를 가진 제곱수의 반복 가능한 시퀀스의 간단한 예입니다:

julia> struct Squares
           count::Int
       end

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

iterate 정의만으로도 Squares 타입은 이미 꽤 강력합니다. 우리는 모든 요소를 반복할 수 있습니다:

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

우리는 in 또는 sum와 같이 반복 가능한 객체와 함께 작동하는 많은 내장 메서드를 사용할 수 있습니다:

julia> 25 in Squares(10)
true

julia> sum(Squares(100))
338350

이 반복 가능한 컬렉션에 대해 Julia에게 더 많은 정보를 제공하기 위해 확장할 수 있는 몇 가지 방법이 있습니다. Squares 시퀀스의 요소는 항상 Int임을 알고 있습니다. eltype 메서드를 확장함으로써, 우리는 Julia에게 그 정보를 제공하고 더 복잡한 메서드에서 더 전문화된 코드를 생성하는 데 도움을 줄 수 있습니다. 또한, 시퀀스의 요소 수를 알고 있으므로 length도 확장할 수 있습니다.

julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type

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

이제 우리가 줄리아에게 collect 모든 요소를 배열로 요청하면, 단순히 push! 각 요소를 Vector{Any}에 추가하는 대신 올바른 크기의 Vector{Int}를 미리 할당할 수 있습니다:

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

우리는 일반적인 구현에 의존할 수 있지만, 더 간단한 알고리즘이 있다는 것을 아는 특정 메서드를 확장할 수도 있습니다. 예를 들어, 제곱의 합을 계산하는 공식이 있으므로, 일반적인 반복 버전을 더 성능이 좋은 솔루션으로 재정의할 수 있습니다:

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

julia> sum(Squares(1803))
1955361914

이것은 Julia Base 전반에 걸쳐 매우 일반적인 패턴입니다: 소수의 필수 메서드가 비공식 인터페이스를 정의하여 더 많은 고급 동작을 가능하게 합니다. 경우에 따라, 타입은 특정 경우에 더 효율적인 알고리즘을 사용할 수 있을 때 추가적인 동작을 전문화하고자 할 것입니다.

컬렉션을 역순으로 반복할 수 있도록 하는 것도 종종 유용합니다. Iterators.reverse(iterator)를 반복함으로써 가능합니다. 그러나 실제로 역순 반복을 지원하려면, 반복자 유형 TIterators.Reverse{T}에 대해 iterate를 구현해야 합니다. (주어진 r::Iterators.Reverse{T}에서, 기본 반복자 유형 Tr.itr입니다.) 우리의 Squares 예제에서는 Iterators.Reverse{Squares} 메서드를 구현할 것입니다:

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

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

Indexing

Methods to implementBrief description
getindex(X, i)X[i], indexed access, non-scalar i should allocate a copy
setindex!(X, v, i)X[i] = v, indexed assignment
firstindex(X)The first index, used in X[begin]
lastindex(X)The last index, used in X[end]

For the Squares iterable above, we can easily compute the ith element of the sequence by squaring it. We can expose this as an indexing expression S[i]. To opt into this behavior, Squares simply needs to define getindex:

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

julia> Squares(100)[23]
529

또한, 구문 S[begin]S[end]를 지원하기 위해 firstindexlastindex를 정의하여 각각 첫 번째 및 마지막 유효 인덱스를 지정해야 합니다:

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

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

julia> Squares(23)[end]
529

다차원 begin/end 인덱싱을 위해 a[3, begin, 7]와 같은 예에서, firstindex(a, dim)lastindex(a, dim)을 정의해야 합니다(기본적으로 각각 axes(a, dim)에서 firstlast를 호출합니다).

그러나 위의 내용은 getindex를 하나의 정수 인덱스만으로 정의한다는 점에 유의해야 합니다. Int 이외의 다른 것으로 인덱싱하면 MethodError가 발생하여 일치하는 메서드가 없다는 오류가 발생합니다. 범위나 Int 벡터로 인덱싱을 지원하기 위해서는 별도의 메서드를 작성해야 합니다:

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

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

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

이제 indexing operations supported by some of the builtin types를 더 많이 지원하기 시작했지만, 여전히 누락된 동작이 상당수 있습니다. 이 Squares 시퀀스는 동작을 추가함에 따라 점점 더 벡터처럼 보이기 시작했습니다. 이러한 모든 동작을 직접 정의하는 대신, 이를 AbstractArray의 하위 유형으로 공식적으로 정의할 수 있습니다.

Abstract Arrays

Methods to implementBrief description
size(A)Returns a tuple containing the dimensions of A
getindex(A, i::Int)(if IndexLinear) Linear scalar indexing
getindex(A, I::Vararg{Int, N})(if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexing
Optional methodsDefault definitionBrief description
IndexStyle(::Type)IndexCartesian()Returns either IndexLinear() or IndexCartesian(). See the description below.
setindex!(A, v, i::Int)(if IndexLinear) Scalar indexed assignment
setindex!(A, v, I::Vararg{Int, N})(if IndexCartesian, where N = ndims(A)) N-dimensional scalar indexed assignment
getindex(A, I...)defined in terms of scalar getindexMultidimensional and nonscalar indexing
setindex!(A, X, I...)defined in terms of scalar setindex!Multidimensional and nonscalar indexed assignment
iteratedefined in terms of scalar getindexIteration
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 size dims
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 of AbstractUnitRange{<:Integer} of valid indices. The axes should be their own axes, that is axes.(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 indices inds (see below)
similar(T::Union{Type,Function}, inds)T(Base.to_shape(inds))Return an array similar to T with the specified indices inds (see below)

AbstractArray의 하위 유형으로 정의된 경우, 단일 요소 접근 위에 구축된 반복 및 다차원 인덱싱을 포함하여 매우 방대한 세트의 풍부한 동작을 상속받습니다. 지원되는 더 많은 메서드는 arrays manual pageJulia Base section를 참조하십시오.

AbstractArray 서브타입을 정의하는 데 중요한 부분은 IndexStyle입니다. 인덱싱은 배열의 중요한 부분이며 종종 핫 루프에서 발생하기 때문에, 인덱싱과 인덱스 할당을 가능한 한 효율적으로 만드는 것이 중요합니다. 배열 데이터 구조는 일반적으로 두 가지 방법 중 하나로 정의됩니다: 하나의 인덱스를 사용하여 요소에 가장 효율적으로 접근하거나(선형 인덱싱) 모든 차원에 대해 지정된 인덱스를 사용하여 본질적으로 요소에 접근합니다. 이 두 가지 방식은 Julia에서 IndexLinear()IndexCartesian()으로 식별됩니다. 선형 인덱스를 여러 인덱싱 서브스크립트로 변환하는 것은 일반적으로 매우 비쌉니다. 따라서 이는 모든 배열 유형에 대해 효율적인 일반 코드를 가능하게 하는 특성 기반 메커니즘을 제공합니다.

이 구분은 타입이 정의해야 하는 스칼라 인덱싱 방법을 결정합니다. IndexLinear() 배열은 간단합니다: getindex(A::ArrayType, i::Int)만 정의하면 됩니다. 배열이 이후 다차원 인덱스 집합으로 인덱싱될 때, 기본 제공되는 getindex(A::AbstractArray, I...)는 인덱스를 하나의 선형 인덱스로 효율적으로 변환한 다음 위의 메서드를 호출합니다. 반면 IndexCartesian() 배열은 ndims(A) Int 인덱스에 대해 지원되는 각 차원에 대해 메서드를 정의해야 합니다. 예를 들어, SparseArrays 표준 라이브러리 모듈의 SparseMatrixCSC는 두 차원만 지원하므로 getindex(A::SparseMatrixCSC, i::Int, j::Int)만 정의합니다. setindex!도 마찬가지입니다.

위의 제곱수 시퀀스로 돌아가면, 대신 AbstractArray{Int, 1}의 하위 유형으로 정의할 수 있습니다:

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

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

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

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

AbstractArray의 두 매개변수를 지정하는 것이 매우 중요하다는 점에 유의하십시오. 첫 번째 매개변수는 eltype를 정의하고, 두 번째 매개변수는 ndims를 정의합니다. 그 슈퍼타입과 이 세 가지 메서드만 있으면 SquaresVector는 반복 가능하고, 인덱스 가능하며, 완전히 기능적인 배열이 됩니다:

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

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

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

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

더 복잡한 예로, Dict 위에 구축된 자체 장난감 N차원 희소 배열 유형을 정의해 보겠습니다:

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

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

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

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

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

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

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

이것이 IndexCartesian 배열임을 주목하십시오. 따라서 우리는 배열의 차원에서 getindexsetindex!를 수동으로 정의해야 합니다. SquaresVector와는 달리, 우리는 4d61726b646f776e2e436f64652822222c2022736574696e646578212229_40726566를 정의할 수 있으므로 배열을 변형할 수 있습니다:

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 an AbstractArray can itself be an array (for instance when indexing by an AbstractRange). The AbstractArray fallback methods use similar to allocate an Array 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

이 예제에서는 Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where T를 정의하여 적절한 래핑 배열을 생성함으로써 달성됩니다. (similar이 1개 및 2개 인수 형태를 지원하지만, 대부분의 경우 3개 인수 형태만 특수화하면 됩니다.) 이를 위해서는 SparseArray가 가변적이어야 하며(setindex!를 지원해야 함) SparseArray에 대해 similar, getindexsetindex!를 정의하면 배열을 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

위의 모든 반복 가능하고 인덱스 가능한 메서드 외에도, 이러한 유형들은 서로 상호작용할 수 있으며 AbstractArrays에 대해 Julia Base에서 정의된 대부분의 메서드를 사용할 수 있습니다:

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

julia> sum(A)
45.0

비전통적인 인덱싱(1이 아닌 다른 숫자에서 시작하는 인덱스)을 허용하는 배열 유형을 정의하는 경우, axes를 특수화해야 합니다. 또한 similar를 특수화하여 dims 인수(일반적으로 Dims 크기 튜플)가 AbstractUnitRange 객체를 수용할 수 있도록 해야 합니다. 아마도 당신이 설계한 범위 유형 Ind일 수 있습니다. 더 많은 정보는 Arrays 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. If A is an AbstractArray{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.

스트라이드 배열은 메모리에 고정된 스트라이드로 저장된 항목을 가진 AbstractArray의 하위 유형입니다. 배열의 요소 유형이 BLAS와 호환되는 경우, 스트라이드 배열은 보다 효율적인 선형 대수 루틴을 위해 BLAS 및 LAPACK 루틴을 활용할 수 있습니다. 사용자 정의 스트라이드 배열의 전형적인 예는 추가 구조로 표준 Array를 래핑한 것입니다.

경고: 기본 저장소가 실제로 스트라이드가 아닌 경우 이러한 메서드를 구현하지 마십시오. 잘못된 결과나 세그멘테이션 오류가 발생할 수 있습니다.

여기 어떤 배열이 스트라이드 배열이고 어떤 배열이 아닌지를 보여주는 몇 가지 예가 있습니다:

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 of SrcType
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 of x, as per axes(x).
Base.broadcastable(x)Convert x to an object that has axes and supports indexing
Bypassing default machinery
Base.copy(bc::Broadcasted{DestStyle})Custom implementation of broadcast
Base.copyto!(dest, bc::Broadcasted{DestStyle})Custom implementation of broadcast!, specializing on DestStyle
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing})Custom implementation of broadcast!, specializing on DestType
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

Broadcastingbroadcast 또는 broadcast!에 대한 명시적 호출에 의해 트리거되거나, A .+ b 또는 f.(x, y)와 같은 "점" 연산에 의해 암묵적으로 트리거됩니다. axes를 가지고 있고 인덱싱을 지원하는 모든 객체는 브로드캐스팅의 인수로 참여할 수 있으며, 기본적으로 결과는 Array에 저장됩니다. 이 기본 프레임워크는 세 가지 주요 방법으로 확장 가능합니다:

  • 모든 인수가 브로드캐스트를 지원하도록 보장하기
  • 주어진 인수 집합에 적합한 출력 배열 선택하기
  • 주어진 인수 집합에 대한 효율적인 구현 선택

모든 유형이 axes 및 인덱싱을 지원하는 것은 아니지만, 많은 유형이 브로드캐스트에서 허용되는 것이 편리합니다. Base.broadcastable 함수는 브로드캐스트할 각 인수에 대해 호출되어, axes 및 인덱싱을 지원하는 다른 것을 반환할 수 있습니다. 기본적으로, 이는 모든 AbstractArrayNumber에 대한 항등 함수입니다. — 이들은 이미 axes 및 인덱싱을 지원합니다.

만약 어떤 타입이 "0차원 스칼라" (단일 객체)처럼 작동하도록 의도되었다면, 다음과 같은 메서드를 정의해야 합니다:

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

인수는 0차원 Ref 컨테이너에 래핑되어 반환됩니다. 예를 들어, 이러한 래퍼 메서드는 타입 자체, 함수, missingnothing와 같은 특별한 싱글톤 및 날짜에 대해 정의됩니다.

사용자 정의 배열 유사 타입은 Base.broadcastable을 전문화하여 자신의 형태를 정의할 수 있지만, collect(Base.broadcastable(x)) == collect(x)라는 규칙을 따라야 합니다. 주목할 만한 예외는 AbstractString입니다. 문자열은 문자들의 반복 가능한 컬렉션임에도 불구하고 브로드캐스트의 목적을 위해 스칼라처럼 동작하도록 특별히 처리됩니다(자세한 내용은 Strings을 참조하십시오).

다음 두 단계(출력 배열 선택 및 구현)는 주어진 인수 집합에 대한 단일 답변을 결정하는 데 의존합니다. 브로드캐스트는 모든 다양한 유형의 인수를 수집하여 단 하나의 출력 배열과 하나의 구현으로 축소해야 합니다. 브로드캐스트는 이 단일 답변을 "스타일"이라고 부릅니다. 각 브로드캐스트 가능한 객체는 고유한 선호 스타일을 가지고 있으며, 이러한 스타일을 단일 답변인 "목적지 스타일"로 결합하기 위해 프로모션과 유사한 시스템이 사용됩니다.

Broadcast Styles

Base.BroadcastStyle는 모든 브로드캐스트 스타일이 파생되는 추상 유형입니다. 함수로 사용될 때 두 가지 가능한 형태가 있으며, 단항(단일 인수) 및 이항입니다. 단항 변형은 특정 브로드캐스트 동작 및/또는 출력 유형을 구현할 의도가 있음을 나타내며, 기본 폴백 Broadcast.DefaultArrayStyle에 의존하고 싶지 않음을 나타냅니다.

이 기본값을 재정의하려면, 객체에 대한 사용자 정의 BroadcastStyle을 정의할 수 있습니다:

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

경우에 따라 MyStyle을 정의하지 않는 것이 편리할 수 있으며, 이 경우 일반 방송 래퍼 중 하나를 활용할 수 있습니다:

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}()는 임의의 타입에 사용할 수 있습니다.
  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}()MyTypeAbstractArray인 경우 선호됩니다.
  • N 차원만 지원하는 AbstractArrays의 경우, Broadcast.AbstractArrayStyle{N}의 하위 유형을 생성합니다.

방송 작업이 여러 인수를 포함할 때, 개별 인수 스타일이 결합되어 출력 컨테이너의 유형을 제어하는 단일 DestStyle이 결정됩니다. 자세한 내용은 below를 참조하세요.

Selecting an appropriate output array

브로드캐스트 스타일은 디스패치 및 특수화를 허용하기 위해 모든 브로드캐스팅 작업에 대해 계산됩니다. 결과 배열의 실제 할당은 similar에 의해 처리되며, 브로드캐스트된 객체가 첫 번째 인수로 사용됩니다.

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

대체 정의는

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

그러나 필요하다면 이러한 인수 중 하나 또는 모두에 대해 전문화할 수 있습니다. 마지막 인수 bc는 (잠재적으로 융합된) 브로드캐스트 작업의 게으른 표현인 Broadcasted 객체입니다. 이러한 목적을 위해 래퍼의 가장 중요한 필드는 함수와 인수 목록을 설명하는 fargs입니다. 인수 목록에는 다른 중첩된 Broadcasted 래퍼가 포함될 수 있으며, 실제로 자주 포함됩니다.

완전한 예를 들자면, ArrayAndChar라는 타입을 생성하여 배열과 단일 문자를 저장했다고 가정해 보겠습니다:

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

브로드캐스팅이 char "메타데이터"를 보존하도록 하려면, 먼저 정의합니다.

Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()

이는 우리가 해당 similar 메서드도 정의해야 함을 의미합니다:

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType
    # 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) = x
find_aac(::Tuple{}) = nothing
find_aac(a::ArrayAndChar, rest) = a
find_aac(::Any, rest) = find_aac(rest)

이 정의들로부터 다음과 같은 행동을 얻을 수 있다:

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 broadcast with custom implementations

일반적으로, 브로드캐스트 작업은 적용할 함수와 그 인수를 함께 보유하는 게으른 Broadcasted 컨테이너로 표현됩니다. 이러한 인수는 더 중첩된 Broadcasted 컨테이너일 수 있으며, 평가될 큰 표현식 트리를 형성합니다. 중첩된 Broadcasted 컨테이너의 트리는 암시적 점 구문을 통해 직접 구성됩니다; 예를 들어, 5 .+ 2.*x는 일시적으로 Broadcasted(+, 5, Broadcasted(*, 2, x))로 표현됩니다. 이는 사용자가 즉시 copy 호출을 통해 실현되기 때문에 보이지 않지만, 이 컨테이너가 사용자 정의 유형의 저자에게 브로드캐스트의 확장성을 제공하는 기초를 제공합니다. 내장된 브로드캐스트 기계는 그런 다음 인수에 따라 결과 유형과 크기를 결정하고, 이를 할당한 다음, 기본 copyto!(::AbstractArray, ::Broadcasted) 메서드를 사용하여 Broadcasted 객체의 실현을 복사합니다. 내장된 폴백 broadcastbroadcast! 메서드도 유사하게 작업의 일시적인 Broadcasted 표현을 구성하여 동일한 코드 경로를 따를 수 있도록 합니다. 이를 통해 사용자 정의 배열 구현이 브로드캐스팅을 사용자화하고 최적화하기 위해 자체 copyto! 특수화를 제공할 수 있습니다. 이는 계산된 브로드캐스트 스타일에 의해 다시 결정됩니다. 이는 작업의 매우 중요한 부분으로, Broadcasted 유형의 첫 번째 유형 매개변수로 저장되어 디스패치 및 특수화를 가능하게 합니다.

일부 유형의 경우, 중첩된 브로드캐스팅 수준에서 연산을 "융합"하는 기계가 없거나 점진적으로 더 효율적으로 수행될 수 있습니다. 이러한 경우, x .* (x .+ 1)broadcast(*, x, broadcast(+, x, 1))로 작성된 것처럼 평가해야 할 수도 있습니다. 여기서 내부 연산은 외부 연산을 처리하기 전에 평가됩니다. 이러한 종류의 즉각적인 연산은 약간의 간접 지원을 통해 직접 지원됩니다. Julia는 융합된 표현식 x .* (x .+ 1)Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1))로 낮춥니다. 이제 기본적으로 broadcasted는 융합된 표현식 트리의 지연 표현을 생성하기 위해 Broadcasted 생성자를 호출하지만, 특정 함수와 인수 조합에 대해 이를 재정의하도록 선택할 수 있습니다.

예를 들어, 내장된 AbstractRange 객체는 시작, 단계 및 길이(또는 정지)를 기준으로 순수하게 즉시 평가할 수 있는 브로드캐스트된 표현의 조각을 최적화하기 위해 이 기계를 사용합니다. 다른 모든 기계와 마찬가지로 broadcasted는 인수의 결합된 브로드캐스트 스타일을 계산하고 노출하므로, broadcasted(f, args...)에 특화하는 대신 broadcasted(::DestStyle, f, args...)에 대해 스타일, 함수 및 인수의 조합에 따라 특화할 수 있습니다.

예를 들어, 다음 정의는 범위의 부정을 지원합니다:

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

Extending in-place broadcasting

인플레이스 브로드캐스팅은 적절한 copyto!(dest, bc::Broadcasted) 메서드를 정의함으로써 지원될 수 있습니다. dest 또는 bc의 특정 하위 유형에 대해 특수화하고 싶을 수 있으므로 패키지 간의 모호성을 피하기 위해 다음과 같은 규칙을 권장합니다.

특정 스타일 DestStyle에 특화하고 싶다면, 메서드를 정의하세요.

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

선택적으로, 이 양식을 사용하여 dest의 유형에 대해서도 전문화할 수 있습니다.

대신 DestStyle에 대한 특수화 없이 목적지 유형 DestType에 특화하고 싶다면, 다음 서명을 가진 메서드를 정의해야 합니다:

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

이것은 래퍼를 Broadcasted{Nothing}으로 변환하는 copyto!의 대체 구현을 활용합니다. 결과적으로 DestType에 대한 전문화는 DestStyle에 대한 전문화보다 우선 순위가 낮습니다.

유사하게, copy(::Broadcasted) 메서드를 사용하여 부적절한 브로드캐스팅을 완전히 무시할 수 있습니다.

Working with Broadcasted objects

이러한 copy 또는 copyto! 메서드를 구현하기 위해서는 물론 각 요소를 계산하기 위해 Broadcasted 래퍼와 함께 작업해야 합니다. 이를 수행하는 두 가지 주요 방법이 있습니다:

  • Broadcast.flatten는 잠재적으로 중첩된 연산을 단일 함수와 평면 인수 목록으로 다시 계산합니다. 방송 모양 규칙을 직접 구현해야 하지만, 이는 제한된 상황에서 유용할 수 있습니다.
  • axes(::Broadcasted)CartesianIndices를 반복하고 결과 CartesianIndex 객체로 인덱싱하여 결과를 계산합니다.

Writing binary broadcasting rules

우선순위 규칙은 이진 BroadcastStyle 호출에 의해 정의됩니다:

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

Style12Style1Style2의 인수를 포함하는 출력에 대해 선택하고자 하는 BroadcastStyle입니다. 예를 들어,

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

Tuple가 0차원 배열보다 "우선"한다는 것을 나타냅니다(출력 컨테이너는 튜플이 됩니다). 사용자가 인수를 어떤 순서로 제공하든 관계없이 하나의 인수 순서만 정의하면 충분하다는 점에 유의해야 합니다.

AbstractArray 유형의 경우, BroadcastStyle을 정의하면 기본 선택인 Broadcast.DefaultArrayStyle를 대체합니다. DefaultArrayStyle과 추상 슈퍼타입인 AbstractArrayStyle은 고정된 차원 요구 사항이 있는 특수 배열 유형을 지원하기 위해 차원 수를 유형 매개변수로 저장합니다.

DefaultArrayStyle는 다음 메서드들 때문에 정의된 다른 모든 AbstractArrayStyle에 "패배"합니다:

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

이진 BroadcastStyle 규칙을 작성할 필요는 없지만, 두 개 이상의 비-DefaultArrayStyle 유형에 대한 우선 순위를 설정하려는 경우에는 작성할 수 있습니다.

배열 유형이 고정 차원 요구 사항이 있는 경우 AbstractArrayStyle을 서브타입으로 만들어야 합니다. 예를 들어, 희소 배열 코드는 다음과 같은 정의를 가지고 있습니다:

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

AbstractArrayStyle를 서브타입화할 때마다, Val(N) 인수를 받는 스타일의 생성자를 만들어 차원 결합 규칙을 정의해야 합니다. 예를 들어:

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

이 규칙은 SparseVecStyle과 0차 또는 1차 배열의 조합이 또 다른 SparseVecStyle을 생성하고, 2차원 배열과의 조합이 SparseMatStyle을 생성하며, 더 높은 차원의 경우 밀집 임의 차원 프레임워크로 돌아간다는 것을 나타냅니다. 이러한 규칙은 브로드캐스팅이 1차원 또는 2차원 출력을 생성하는 연산에 대해 희소 표현을 유지하도록 허용하지만, 다른 차원에 대해서는 Array를 생성합니다.

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 object x. If private=true, also return property names intended to be kept as private
getproperty(x::ObjType, s::Symbol)getfield(x, s)Return property s of x. x.s calls getproperty(x, :s).
setproperty!(x::ObjType, s::Symbol, v)setfield!(x, s, v)Set property s of x to v. x.s = v calls setproperty!(x, :s, v). Should return v.

때때로 최종 사용자가 객체의 필드와 상호작용하는 방식을 변경하는 것이 바람직할 수 있습니다. 타입 필드에 대한 직접적인 접근을 허용하는 대신, 사용자와 코드 사이에 추가적인 추상화 계층을 제공하기 위해 object.field를 오버로딩할 수 있습니다. 속성은 사용자가 객체에서 보는 것이고, 필드는 객체가 실제로 무엇인지입니다.

기본적으로 속성과 필드는 동일합니다. 그러나 이 동작은 변경할 수 있습니다. 예를 들어, polar coordinates에서 평면의 점을 나타내는 이 표현을 살펴보세요:

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

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

위의 표에 설명된 대로 점 접근 p.r은 기본적으로 getproperty(p, :r)와 같으며, 이는 기본적으로 getfield(p, :r)와 같습니다:

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

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

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

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

그러나 우리는 사용자가 Point가 좌표를 rϕ(필드)로 저장한다는 사실을 모르게 하고 대신 xy(속성)와 상호작용하기를 원할 수 있습니다. 첫 번째 열의 메서드는 새로운 기능을 추가하도록 정의될 수 있습니다:

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
       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
               # This allow modifying fields with p.r and p.ϕ
               return setfield!(p, s, f)
           end
       end

getpropertysetproperty! 내부에서 getfieldsetfield를 사용하는 것이 중요합니다. 점(.) 구문을 사용하면 함수가 재귀적으로 호출되어 타입 추론 문제를 일으킬 수 있습니다. 이제 새로운 기능을 시도해 볼 수 있습니다:

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

julia> p.x
4.949747468305833

julia> p.y = 4.0
4.0

julia> p.r
6.363961030678928

마지막으로, 이렇게 인스턴스 속성을 추가하는 것은 줄리아에서 매우 드물게 이루어지며, 일반적으로 그렇게 하는 데에는 충분한 이유가 있어야 한다는 점을 언급할 가치가 있다.

Rounding

Methods to implementDefault definitionBrief description
round(x::ObjType, r::RoundingMode)noneRound x and return the result. If possible, round should return an object of the same type as x
round(T::Type, x::ObjType, r::RoundingMode)convert(T, round(x, r))Round x, returning the result as a T

새로운 유형에서 반올림을 지원하려면 일반적으로 단일 메서드 round(x::ObjType, r::RoundingMode)를 정의하는 것으로 충분합니다. 전달된 반올림 모드는 값이 어떤 방향으로 반올림되어야 하는지를 결정합니다. 가장 일반적으로 사용되는 반올림 모드는 RoundNearest, RoundToZero, RoundDown, 및 RoundUp이며, 이러한 반올림 모드는 각각 하나의 인수를 가지는 round, 메서드, 및 trunc, floor, ceil의 정의에서 사용됩니다.

일부 경우, 두 개의 인수 메서드 뒤에 변환을 따르는 것보다 더 정확하거나 성능이 좋은 세 개의 인수 round 메서드를 정의하는 것이 가능합니다. 이 경우 두 개의 인수 메서드 외에 세 개의 인수 메서드를 정의하는 것이 허용됩니다. 반올림된 결과를 T 유형의 객체로 표현할 수 없는 경우, 세 개의 인수 메서드는 InexactError를 발생시켜야 합니다.

예를 들어, https://github.com/JuliaPhysics/Measurements.jl와 유사한 가능한 값의 범위를 나타내는 Interval 유형이 있는 경우, 다음과 같이 해당 유형에 대한 반올림을 정의할 수 있습니다.

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)