The NamedDims.jl package adds names to each dimension of a multi-dimensional array. The source code can be found at https://github.com/invenia/NamedDims.jl.
Let's take a look at the definition of NamedDimsArray:
"""
The `NamedDimsArray` constructor takes a list of names as `Symbol`s,
one per dimension, and an array to wrap.
"""
struct NamedDimsArray{L, T, N, A<:AbstractArray{T, N}} <: AbstractArray{T, N}
# `L` is for labels, it should be an `NTuple{N, Symbol}`
data::A
end
Don't be intimidated by the signature. It is actually quite straightforward.
NamedDimsArray is a subtype of the abstract array type AbstractArray{T, N}. It only contains a single field, data, which keeps track of the underlying data. Because T and N are already parameters in A, they also need to be specified in the signature of NamedDimsArray. The L parameter is used to keep track of the names of the dimensions. Note that L is not used in any of the fields but that it is conveniently stored in the type signature itself.
The primary constructor is defined as follows:
function NamedDimsArray{L}(orig::AbstractArray{T, N}) where {L, T, N}
if !(L isa NTuple{N, Symbol})
throw(ArgumentError(
"A $N dimensional array, needs a $N-tuple of dimension names. Got: $L"
))
end
return NamedDimsArray{L, T, N, typeof(orig)}(orig)
end
The function only needs to take an AbstractArray{T,N} that is an N-dimensional array with an element type of T. First, it checks if L contains a tuple of N symbols. Because type parameters are first-class, they can be examined in the body of the function. Assuming that L contains the right number of symbols, it just instantiates a NamedDimsArray using the known parameters L, T, N, as well as the type of the array argument.
It may be easier to see how it's used, so let's take a look:
In the output, we can see that the type signature is NamedDimsArray{(:x, :y),Int64,2,Array{Int64,2}}. Matching this with the signature of the NamedDimsArray type, we can see that L is just the two-symbol tuple (:x, :y), T is Int64, N is 2, and the underlying data is of the Array{Int64, 2} type.
Let's take a look at the dimnames function, which is defined as follows:
dimnames(::Type{<:NamedDimsArray{L}}) where L = L
This function returns the dimensions tuple:
Now, things are getting a little more interesting. What is NamedDimsArray{L}? Didn't we need four parameters in this type? It is worth noting that a type such as NamedDimsArray{L, T, N, A} is actually a subtype of NamedDimsArray{L}. We can prove this as follows:
If we really want to see what NamedDimsArray{L} is, we can try the following:
What seems to be happening is that NamedDimsArray{(:x, :y)} is just shorthand for NamedDimsArray{(:x, :y),T,N,A} where A<:AbstractArray{T,N} where N where T. Because this is a more general type with three unknown parameters, we can see why NamedDimsArray{(:x, :y),Int64,2,Array{Int64,2}} is a subtype of NamedDimsArray{(:x, :y)}.
Using parametric types is very good if we wish to reuse functionalities. We can almost view each type parameter as a "dimension". When a parametric type has two type parameters, we would have many possible subtypes based upon various combinations of each type parameter.