Interfaces

Gran parte del poder y la extensibilidad en Julia proviene de una colección de interfaces informales. Al extender algunos métodos específicos para que funcionen con un tipo personalizado, los objetos de ese tipo no solo reciben esas funcionalidades, sino que también pueden ser utilizados en otros métodos que están escritos para construir genéricamente sobre esos comportamientos.

Iteration

Hay dos métodos que siempre son necesarios:

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

Hay varios métodos más que deben definirse en algunas circunstancias. Tenga en cuenta que siempre debe definir al menos uno de Base.IteratorSize(IterType) y length(iter) porque la definición predeterminada de Base.IteratorSize(IterType) es 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!

La iteración secuencial se implementa mediante la función iterate. En lugar de mutar objetos mientras se iteran, los iteradores de Julia pueden llevar un seguimiento del estado de la iteración externamente al objeto. El valor de retorno de iterate es siempre una tupla de un valor y un estado, o nothing si no quedan elementos. El objeto de estado se pasará de vuelta a la función iterate en la siguiente iteración y generalmente se considera un detalle de implementación privado al objeto iterable.

Cualquier objeto que defina esta función es iterable y se puede usar en el many functions that rely upon iteration. También se puede usar directamente en un bucle for ya que la sintaxis:

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

es se traduce en:

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

Un ejemplo simple es una secuencia iterable de números cuadrados con una longitud definida:

julia> struct Squares
           count::Int
       end

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

Con solo iterate definición, el tipo Squares ya es bastante poderoso. Podemos iterar sobre todos los elementos:

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

Podemos usar muchos de los métodos integrados que funcionan con iterables, como in o sum:

julia> 25 in Squares(10)
true

julia> sum(Squares(100))
338350

Hay algunos métodos más que podemos extender para darle a Julia más información sobre esta colección iterable. Sabemos que los elementos en una secuencia de Squares siempre serán Int. Al extender el método eltype, podemos darle esa información a Julia y ayudarle a generar código más especializado en los métodos más complicados. También sabemos el número de elementos en nuestra secuencia, así que podemos extender length, también:

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

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

Ahora, cuando le pedimos a Julia que collect todos los elementos en un array, puede preasignar un Vector{Int} del tamaño correcto en lugar de ingenuamente push! cada elemento en un Vector{Any}:

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

Mientras podemos confiar en implementaciones genéricas, también podemos extender métodos específicos donde sabemos que hay un algoritmo más simple. Por ejemplo, hay una fórmula para calcular la suma de cuadrados, por lo que podemos anular la versión iterativa genérica con una solución más eficiente:

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

julia> sum(Squares(1803))
1955361914

Este es un patrón muy común en Julia Base: un pequeño conjunto de métodos requeridos define una interfaz informal que permite muchos comportamientos más sofisticados. En algunos casos, los tipos querrán especializar adicionalmente esos comportamientos extra cuando saben que se puede utilizar un algoritmo más eficiente en su caso específico.

También suele ser útil permitir la iteración sobre una colección en orden inverso iterando sobre Iterators.reverse(iterator). Sin embargo, para admitir realmente la iteración en orden inverso, un tipo de iterador T necesita implementar iterate para Iterators.Reverse{T}. (Dado r::Iterators.Reverse{T}, el iterador subyacente de tipo T es r.itr.) En nuestro ejemplo de Squares, implementaríamos los métodos de 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]

Para el iterable Squares mencionado anteriormente, podemos calcular fácilmente el i-ésimo elemento de la secuencia al elevarlo al cuadrado. Podemos exponer esto como una expresión de indexación S[i]. Para optar por este comportamiento, Squares simplemente necesita definir 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

Además, para soportar la sintaxis S[begin] y S[end], debemos definir firstindex y lastindex para especificar los primeros y últimos índices válidos, respectivamente:

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

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

julia> Squares(23)[end]
529

Para la indexación multi-dimensional begin/end como en a[3, begin, 7], por ejemplo, debes definir firstindex(a, dim) y lastindex(a, dim) (que por defecto llaman a first y last en axes(a, dim), respectivamente).

Nota, sin embargo, que lo anterior solo define getindex con un índice entero. Indexar con cualquier cosa que no sea un Int lanzará un MethodError diciendo que no había un método coincidente. Para soportar la indexación con rangos o vectores de Ints, deben escribirse métodos separados:

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

Mientras esto comienza a soportar más de las indexing operations supported by some of the builtin types, todavía hay un buen número de comportamientos que faltan. Esta secuencia de Squares comienza a parecerse más y más a un vector a medida que hemos añadido comportamientos a ella. En lugar de definir todos estos comportamientos nosotros mismos, podemos definirlo oficialmente como un subtipo de un 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)

Si un tipo se define como un subtipo de AbstractArray, hereda un conjunto muy grande de comportamientos ricos, incluyendo la iteración y la indexación multidimensional construida sobre el acceso a un solo elemento. Consulta el arrays manual page y el Julia Base section para más métodos soportados.

Una parte clave en la definición de un subtipo de AbstractArray es IndexStyle. Dado que la indexación es una parte tan importante de un array y a menudo ocurre en bucles críticos, es importante hacer que tanto la indexación como la asignación indexada sean lo más eficientes posible. Las estructuras de datos de array se definen típicamente de una de dos maneras: o accede a sus elementos de la manera más eficiente utilizando solo un índice (indexación lineal) o accede intrínsecamente a los elementos con índices especificados para cada dimensión. Estas dos modalidades son identificadas por Julia como IndexLinear() y IndexCartesian(). Convertir un índice lineal a múltiples subíndices de indexación suele ser muy costoso, por lo que esto proporciona un mecanismo basado en rasgos para habilitar código genérico eficiente para todos los tipos de array.

Esta distinción determina qué métodos de indexación escalar debe definir el tipo. Los arreglos IndexLinear() son simples: solo se define getindex(A::ArrayType, i::Int). Cuando el arreglo se indexa posteriormente con un conjunto multidimensional de índices, el método de respaldo getindex(A::AbstractArray, I...) convierte eficientemente los índices en un índice lineal y luego llama al método anterior. Los arreglos IndexCartesian(), por otro lado, requieren que se definan métodos para cada dimensionalidad soportada con índices Int de ndims(A). Por ejemplo, SparseMatrixCSC del módulo de la biblioteca estándar SparseArrays, solo soporta dos dimensiones, por lo que solo define getindex(A::SparseMatrixCSC, i::Int, j::Int). Lo mismo ocurre con setindex!.

Regresando a la secuencia de cuadrados de arriba, podríamos definirla en su lugar como un subtipo de un 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

Tenga en cuenta que es muy importante especificar los dos parámetros de la AbstractArray; el primero define el eltype, y el segundo define el ndims. Ese supertipo y esos tres métodos son todo lo que se necesita para que SquaresVector sea un array iterable, indexable y completamente funcional:

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

Como un ejemplo más complicado, definamos nuestro propio tipo de arreglo disperso-like N-dimensional construido sobre Dict:

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

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

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

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

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

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

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

Tenga en cuenta que este es un IndexCartesian array, por lo que debemos definir manualmente getindex y setindex! en la dimensionalidad del array. A diferencia del SquaresVector, podemos definir 4d61726b646f776e2e436f64652822222c2022736574696e646578212229_40726566, y así podemos mutar el array:

julia> A = SparseArray(Float64, 3, 3)
3×3 SparseArray{Float64, 2}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> fill!(A, 2)
3×3 SparseArray{Float64, 2}:
 2.0  2.0  2.0
 2.0  2.0  2.0
 2.0  2.0  2.0

julia> A[:] = 1:length(A); A
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

El resultado de indexar un AbstractArray puede ser a su vez un array (por ejemplo, al indexar por un AbstractRange). Los métodos de retroceso de AbstractArray utilizan similar para asignar un Array del tamaño y tipo de elemento apropiados, que se llena utilizando el método de indexación básica descrito anteriormente. Sin embargo, al implementar un envoltorio de array, a menudo deseas que el resultado también esté envuelto:

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

En este ejemplo se logra definiendo Base.similar(A::SparseArray, ::Type{T}, dims::Dims) donde T para crear el arreglo envuelto apropiado. (Nota que aunque similar soporta formas de 1 y 2 argumentos, en la mayoría de los casos solo necesitas especializar la forma de 3 argumentos). Para que esto funcione es importante que SparseArray sea mutable (soporte setindex!). Definir similar, getindex y setindex! para SparseArray también hace posible copy el arreglo:

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

Además de todos los métodos iterables e indexables mencionados anteriormente, estos tipos también pueden interactuar entre sí y utilizar la mayoría de los métodos definidos en Julia Base para AbstractArrays:

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

julia> sum(A)
45.0

Si estás definiendo un tipo de arreglo que permite indexación no tradicional (índices que comienzan en algo diferente a 1), deberías especializar axes. También deberías especializar similar para que el argumento dims (normalmente una tupla de tamaño Dims) pueda aceptar objetos AbstractUnitRange, quizás tipos de rango Ind de tu propio diseño. Para más información, consulta 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.

Un array estratificado es un subtipo de AbstractArray cuyas entradas se almacenan en memoria con pasos fijos. Siempre que el tipo de elemento del array sea compatible con BLAS, un array estratificado puede utilizar rutinas de BLAS y LAPACK para realizar rutinas de álgebra lineal de manera más eficiente. Un ejemplo típico de un array estratificado definido por el usuario es uno que envuelve un Array estándar con una estructura adicional.

Advertencia: no implemente estos métodos si el almacenamiento subyacente no es realmente estratificado, ya que puede llevar a resultados incorrectos o fallos de segmentación.

Aquí hay algunos ejemplos para demostrar qué tipo de arreglos son estratificados y cuáles no:

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

Broadcasting se activa mediante una llamada explícita a broadcast o broadcast!, o implícitamente mediante operaciones de "punto" como A .+ b o f.(x, y). Cualquier objeto que tenga axes y que soporte indexación puede participar como argumento en la transmisión, y por defecto el resultado se almacena en un Array. Este marco básico es extensible de tres maneras principales:

  • Asegurando que todos los argumentos soporten la transmisión
  • Seleccionando un array de salida apropiado para el conjunto de argumentos dado
  • Seleccionando una implementación eficiente para el conjunto de argumentos dado

No todos los tipos admiten axes e indexación, pero muchos son convenientes para permitir en la difusión. La función Base.broadcastable se llama en cada argumento para la difusión, permitiendo que devuelva algo diferente que soporte axes e indexación. Por defecto, esta es la función identidad para todos los AbstractArrays y Numbers: ya admiten axes e indexación.

Si un tipo está destinado a actuar como un "escalar de 0 dimensiones" (un solo objeto) en lugar de como un contenedor para la difusión, entonces se debe definir el siguiente método:

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

que devuelve el argumento envuelto en un contenedor Ref de 0 dimensiones. Por ejemplo, tal método de envoltura está definido para los tipos mismos, funciones, singleton especiales como missing y nothing, y fechas.

Los tipos personalizados similares a arreglos pueden especializar Base.broadcastable para definir su forma, pero deben seguir la convención de que collect(Base.broadcastable(x)) == collect(x). Una excepción notable es AbstractString; las cadenas se tratan de manera especial para comportarse como escalares a efectos de la difusión, aunque son colecciones iterables de sus caracteres (ver Strings para más información).

Los siguientes dos pasos (seleccionar el array de salida e implementación) dependen de determinar una única respuesta para un conjunto dado de argumentos. Broadcast debe tomar todos los tipos variados de sus argumentos y reducirlos a un solo array de salida y una sola implementación. Broadcast llama a esta única respuesta un "estilo". Cada objeto que se puede transmitir tiene su propio estilo preferido, y se utiliza un sistema similar a la promoción para combinar estos estilos en una única respuesta: el "estilo de destino".

Broadcast Styles

Base.BroadcastStyle es el tipo abstracto del cual se derivan todos los estilos de transmisión. Cuando se usa como una función, tiene dos formas posibles: unaria (un solo argumento) y binaria. La variante unaria indica que tienes la intención de implementar un comportamiento de transmisión específico y/o un tipo de salida, y no deseas depender del valor predeterminado de respaldo Broadcast.DefaultArrayStyle.

Para anular estos valores predeterminados, puedes definir un BroadcastStyle personalizado para tu objeto:

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

En algunos casos puede ser conveniente no tener que definir MyStyle, en cuyo caso puedes aprovechar uno de los envoltorios de difusión generales:

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}() se puede usar para tipos arbitrarios.
  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}() se prefiere si MyType es un AbstractArray.
  • Para AbstractArrays que solo admiten una cierta dimensionalidad, crea un subtipo de Broadcast.AbstractArrayStyle{N} (ver abajo).

Cuando tu operación de transmisión involucra varios argumentos, los estilos de argumento individuales se combinan para determinar un único DestStyle que controla el tipo del contenedor de salida. Para más detalles, consulta below.

Selecting an appropriate output array

El estilo de difusión se calcula para cada operación de difusión para permitir la distribución y especialización. La asignación real del array de resultados es manejada por similar, utilizando el objeto Broadcasted como su primer argumento.

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

La definición de respaldo es

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

Sin embargo, si es necesario, puedes especializarte en cualquiera o en todos estos argumentos. El argumento final bc es una representación perezosa de una operación de difusión (potencialmente fusionada), un objeto Broadcasted. Para estos propósitos, los campos más importantes del envoltorio son f y args, que describen la función y la lista de argumentos, respectivamente. Ten en cuenta que la lista de argumentos puede —y a menudo lo hace— incluir otros envoltorios Broadcasted anidados.

Para un ejemplo completo, digamos que has creado un tipo, ArrayAndChar, que almacena un array y un solo carácter:

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, "'")

Es posible que desees que la transmisión preserve los char "metadatos". Primero definimos

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

Esto significa que también debemos definir un método similar correspondiente:

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)

A partir de estas definiciones, se obtiene el siguiente comportamiento:

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

En general, una operación de difusión se representa mediante un contenedor Broadcasted perezoso que retiene la función que se aplicará junto con sus argumentos. Esos argumentos pueden ser a su vez contenedores Broadcasted más anidados, formando un gran árbol de expresiones que se evaluará. Un árbol anidado de contenedores Broadcasted se construye directamente mediante la sintaxis de punto implícita; 5 .+ 2.*x se representa transitoriamente como Broadcasted(+, 5, Broadcasted(*, 2, x)), por ejemplo. Esto es invisible para los usuarios, ya que se realiza inmediatamente a través de una llamada a copy, pero es este contenedor el que proporciona la base para la extensibilidad de la difusión para los autores de tipos personalizados. La maquinaria de difusión incorporada determinará entonces el tipo y tamaño del resultado en función de los argumentos, lo asignará y finalmente copiará la realización del objeto Broadcasted en él con un método predeterminado copyto!(::AbstractArray, ::Broadcasted). Los métodos de broadcast y broadcast! incorporados de respaldo construyen de manera similar una representación transitoria Broadcasted de la operación para que puedan seguir el mismo camino de código. Esto permite que las implementaciones de matrices personalizadas proporcionen su propia especialización de copyto! para personalizar y optimizar la difusión. Esto se determina nuevamente por el estilo de difusión calculado. Esta es una parte tan importante de la operación que se almacena como el primer parámetro de tipo del tipo Broadcasted, lo que permite la dispatch y la especialización.

Para algunos tipos, la maquinaria para "fusionar" operaciones a través de niveles anidados de difusión no está disponible o podría hacerse de manera más eficiente de forma incremental. En tales casos, es posible que necesite o desee evaluar x .* (x .+ 1) como si se hubiera escrito broadcast(*, x, broadcast(+, x, 1)), donde la operación interna se evalúa antes de abordar la operación externa. Este tipo de operación ansiosa es directamente soportada por un poco de indirecta; en lugar de construir directamente objetos Broadcasted, Julia reduce la expresión fusionada x .* (x .+ 1) a Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1)). Ahora, por defecto, broadcasted simplemente llama al constructor Broadcasted para crear la representación perezosa del árbol de expresión fusionada, pero puede elegir anularlo para una combinación particular de función y argumentos.

Como ejemplo, los objetos AbstractRange integrados utilizan esta maquinaria para optimizar partes de expresiones transmitidas que pueden evaluarse de manera anticipada puramente en términos de inicio, paso y longitud (o parada) en lugar de calcular cada elemento individual. Al igual que toda la otra maquinaria, broadcasted también calcula y expone el estilo de transmisión combinado de sus argumentos, por lo que en lugar de especializarse en broadcasted(f, args...), puedes especializarte en broadcasted(::DestStyle, f, args...) para cualquier combinación de estilo, función y argumentos.

Por ejemplo, la siguiente definición admite la negación de rangos:

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

Extending in-place broadcasting

La difusión en el lugar puede ser soportada definiendo el método apropiado copyto!(dest, bc::Broadcasted). Debido a que podrías querer especializarte ya sea en dest o en el subtipo específico de bc, para evitar ambigüedades entre paquetes, recomendamos la siguiente convención.

Si deseas especializarte en un estilo particular DestStyle, define un método para

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

Opcionalmente, con este formulario también puedes especializarte en el tipo de dest.

Si en su lugar desea especializarse en el tipo de destino DestType sin especializarse en DestStyle, entonces debe definir un método con la siguiente firma:

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

Esto aprovecha una implementación de respaldo de copyto! que convierte el envoltorio en un Broadcasted{Nothing}. En consecuencia, la especialización en DestType tiene una menor precedencia que los métodos que se especializan en DestStyle.

De manera similar, puedes anular completamente la transmisión fuera de lugar con un método copy(::Broadcasted).

Working with Broadcasted objects

Para implementar un método copy o copyto!, por supuesto, debes trabajar con el envoltorio Broadcasted para calcular cada elemento. Hay dos formas principales de hacerlo:

  • Broadcast.flatten recomputa la operación potencialmente anidada en una sola función y una lista plana de argumentos. Eres responsable de implementar las reglas de forma de broadcasting tú mismo, pero esto puede ser útil en situaciones limitadas.
  • Iterando sobre los CartesianIndices de los axes(::Broadcasted) y utilizando la indexación con el objeto CartesianIndex resultante para calcular el resultado.

Writing binary broadcasting rules

Las reglas de precedencia se definen mediante llamadas binarias BroadcastStyle:

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

donde Style12 es el BroadcastStyle que deseas elegir para las salidas que involucran argumentos de Style1 y Style2. Por ejemplo,

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

indica que Tuple "gana" sobre los arreglos de cero dimensiones (el contenedor de salida será una tupla). Vale la pena señalar que no necesitas (y no deberías) definir ambos órdenes de argumentos de esta llamada; definir uno es suficiente sin importar el orden en que el usuario suministre los argumentos.

Para los tipos AbstractArray, definir un BroadcastStyle reemplaza la elección por defecto, Broadcast.DefaultArrayStyle. DefaultArrayStyle y el supertipo abstracto, AbstractArrayStyle, almacenan la dimensionalidad como un parámetro de tipo para soportar tipos de arreglos especializados que tienen requisitos de dimensionalidad fijos.

DefaultArrayStyle "pierde" ante cualquier otro AbstractArrayStyle que se haya definido debido a los siguientes métodos:

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

No necesitas escribir reglas de BroadcastStyle binarias a menos que desees establecer precedencia para dos o más tipos que no sean DefaultArrayStyle.

Si su tipo de matriz tiene requisitos de dimensionalidad fija, entonces debe subtipar AbstractArrayStyle. Por ejemplo, el código de matriz dispersa tiene las siguientes definiciones:

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

Siempre que subclasifiques AbstractArrayStyle, también necesitas definir reglas para combinar dimensionalidades, creando un constructor para tu estilo que tome un argumento Val(N). Por ejemplo:

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

Estas reglas indican que la combinación de un SparseVecStyle con arreglos de 0 o 1 dimensión produce otro SparseVecStyle, que su combinación con un arreglo de 2 dimensiones produce un SparseMatStyle, y cualquier cosa de mayor dimensionalidad vuelve al marco denso de dimensiones arbitrarias. Estas reglas permiten que la transmisión mantenga la representación dispersa para operaciones que resultan en salidas de una o dos dimensiones, pero producen un Array para cualquier otra dimensionalidad.

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.

A veces, es deseable cambiar cómo el usuario final interactúa con los campos de un objeto. En lugar de otorgar acceso directo a los campos de tipo, se puede proporcionar una capa adicional de abstracción entre el usuario y el código sobrecargando object.field. Las propiedades son lo que el usuario ve de el objeto, los campos lo que el objeto realmente es.

Por defecto, las propiedades y los campos son lo mismo. Sin embargo, este comportamiento se puede cambiar. Por ejemplo, toma esta representación de un punto en un plano en polar coordinates:

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

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

Como se describe en la tabla anterior, el acceso por punto p.r es lo mismo que getproperty(p, :r), que por defecto es lo mismo que 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)

Sin embargo, es posible que queramos que los usuarios no sean conscientes de que Point almacena las coordenadas como r y ϕ (campos), y en su lugar interactúen con x y y (propiedades). Los métodos en la primera columna se pueden definir para agregar nueva funcionalidad:

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

Es importante que getfield y setfield se utilicen dentro de getproperty y setproperty! en lugar de la sintaxis de punto, ya que la sintaxis de punto haría que las funciones fueran recursivas, lo que puede llevar a problemas de inferencia de tipos. Ahora podemos probar la nueva funcionalidad:

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

julia> p.x
4.949747468305833

julia> p.y = 4.0
4.0

julia> p.r
6.363961030678928

Finalmente, vale la pena señalar que agregar propiedades de instancia de esta manera se hace bastante raramente en Julia y, en general, solo debe hacerse si hay una buena razón para hacerlo.

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

Para soportar el redondeo en un nuevo tipo, generalmente es suficiente definir el único método round(x::ObjType, r::RoundingMode). El modo de redondeo pasado determina en qué dirección debe redondearse el valor. Los modos de redondeo más comúnmente utilizados son RoundNearest, RoundToZero, RoundDown y RoundUp, ya que estos modos de redondeo se utilizan en las definiciones del método de un argumento round, y trunc, floor y ceil, respectivamente.

En algunos casos, es posible definir un método round de tres argumentos que sea más preciso o eficiente que el método de dos argumentos seguido de la conversión. En este caso, es aceptable definir el método de tres argumentos además del método de dos argumentos. Si es imposible representar el resultado redondeado como un objeto del tipo T, entonces el método de tres argumentos debe lanzar un InexactError.

Por ejemplo, si tenemos un tipo Interval que representa un rango de valores posibles similar a https://github.com/JuliaPhysics/Measurements.jl, podemos definir el redondeo en ese tipo con lo siguiente

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)