Skip to content

Commit 1966706

Browse files
authored
ArrayInterface.axes docs, clean up, and ReinterpretArray support. (#222)
* +axis support methods from base and +tests * Get rid of some no good very bad spaghetti code. * Document examples of how to use axes here. * Incorporate ReinterpretArray changes from ArrayInterface.axes for ReshapedReinterpretArray #210. Don't assume we can go to parent arrays anymore because there are too many assumptions (offsets, sizing, views, etc) that can change it. * Document `SoneTo`
1 parent 5987bae commit 1966706

File tree

11 files changed

+401
-314
lines changed

11 files changed

+401
-314
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ArrayInterface"
22
uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
3-
version = "3.1.36"
3+
version = "3.1.37"
44

55
[deps]
66
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ ArrayInterface.BroadcastAxis
8181
ArrayInterface.LazyAxis
8282
ArrayInterface.OptionallyStaticStepRange
8383
ArrayInterface.OptionallyStaticUnitRange
84+
ArrayInteraface.SOneTo
8485
ArrayInterface.StrideIndex
8586
```
8687

docs/src/index.md

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Designs for new Base array interface primitives, used widely through scientific
88

99
## Inheriting Array Traits
1010

11-
Creating an array type with unique behavior in Julia is often accomplished by creating a lazy wrapper around previously defined array types.
11+
Creating an array type with unique behavior in Julia is often accomplished by creating a lazy wrapper around previously defined array types (e.g. [composition by inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance)).
1212
This allows the new array type to inherit functionality by redirecting methods to the parent array (e.g., `Base.size(x::Wrapper) = size(parent(x))`).
1313
Generic design limits the need to define an excessive number of methods like this.
1414
However, methods used to describe a type's traits often need to be explicitly defined for each trait method.
@@ -32,7 +32,7 @@ Most traits in `ArrayInterface` are a variant on this pattern.
3232

3333
## Static Traits
3434

35-
The size along one or more dimensions of an array may be known at compile time.
35+
The size along one or more dimensions of an array may be known at compile time.
3636
`ArrayInterface.known_size` is useful for extracting this information from array types and `ArrayInterface.size` is useful for extracting this information from an instance of an array.
3737
For example:
3838

@@ -94,6 +94,7 @@ If `x`'s first dimension is named `:dim_1` then calling `f(x, :dim_1)` would res
9494
If users knew they always wanted to call `f(x, 2)` then they could define `h(x) = f(x, static(2))`, ensuring `f` passes along that information while compiling.
9595

9696
New types defining dimension names can do something similar to:
97+
9798
```julia
9899
using Static
99100
using ArrayInterface
@@ -107,4 +108,69 @@ Dimension names should be appropriately propagated between nested arrays using `
107108
This allows types such as `SubArray` and `PermutedDimsArray` to work with named dimensions.
108109
Similarly, other methods that return information corresponding to dimensions (e.g., `ArrayInterfce.size`, `ArrayInterface.axes`) use `to_parent_dims` to appropriately propagate parent information.
109110

111+
## Axes
112+
113+
Where Julia's currently documented [array interface]( https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array) requires defining `Base.size`, ArrayInterface instead requires defining [`ArrayInterface.axes`](@ref) and [`ArrayInterface.axes_types`](@ref).
114+
`ArrayInterface.axes_types(::Type{T})` facilitates propagation of a number of traits known at compile time (e.g., `known_size`, `known_offsets`) and `ArrayInterface.axes(::AbstractArray)` replaces `Base.OneTo` with `ArrayInterface.OptionallyStaticUnitRange` in situations where static information would otherwise be lost.
115+
`ArrayInterface.axes(::AbstractArray, dim)` utilizes `to_dims`, [as described elsewhere](#dimensions).
116+
117+
### Simple Wrappers
118+
119+
Let's say we have a new array type doesn't affect axes then this is as simple as:
120+
```julia
121+
Base.axes(x::SimpleWrapper) = ArrayInterface.axes(parent(x))
122+
Base.axes(x::SimpleWrapper, dim) = ArrayInterface.axes(parent(x), dim)
123+
ArrayInterface.axes_types(::Type{T}) where {T<:SimpleWrapper} = axes_types(parent_type(T))
124+
```
125+
126+
To reiterate, `ArrayInterface.axes` improves on `Base.axes` for few Base array types but is otherwise identical.
127+
Therefore, the first method simply ensures you don't have to define multiple parametric methods for your new type to preserve statically sized nested axes (e.g., `SimpleWrapper{T,N,<:Transpose{T,<:AbstractVector}}`).
128+
This is otherwise identical to standard inheritance by composition.
129+
130+
### When to Discard Axis Information
131+
132+
Occasionally the parent array's axis information can't be preserved.
133+
For example, we can't map axis information from the parent array of `Base.ReshapedArray`.
134+
In this case we can simply build axes from the new size information.
135+
136+
```julia
137+
ArrayInterface.axes_types(T::Type{<:ReshapedArray}) = NTuple{ndims(T),OneTo{Int}}
138+
ArrayInterface.axes(A::ReshapedArray) = map(OneTo, size(A))
139+
```
140+
141+
### New Axis Types
142+
143+
`OffsetArray` changes the first index for each axis.
144+
It produces axes of type `IdOffsetRange`, which contains the value of the relative offset and the parent axis.
145+
146+
```julia
147+
using ArrayInterface: axes_types, parent_type, to_dims
148+
# Note that generating a `Tuple` type piecewise like may be type unstable and should be
149+
# tested using `Test.@inferred`. It's often necessary to use generated function
150+
# (`@generated`) or methods defined in Static.jl.
151+
@generated function ArrayInterface.axes_types(::Type{A}) where {A<:OffsetArray}
152+
out = Expr(:curly, :Tuple)
153+
P = parent_type(A)
154+
for dim in 1:ndims(A)
155+
# offset relative to parent array
156+
O = relative_known_offsets(A, dim)
157+
if O === nothing # offset is not known at compile time and is an `Int`
158+
push!(out.args, :(IdOffsetRange{Int, axes_types($P, $(static(dim)))}))
159+
else # offset is known, therefore it is a `StaticInt`
160+
push!(out.args, :(IdOffsetRange{StaticInt{$O}, axes_types($P, $(static(dim))}))
161+
end
162+
end
163+
end
164+
function Base.axes(A::OffsetArray)
165+
map(IdOffsetRange, ArrayInterface.axes(parent(A)), relative_offsets(A))
166+
end
167+
function Base.axes(A::OffsetArray, dim)
168+
d = to_dims(A, dim)
169+
IdOffsetRange(ArrayInterface.axes(parent(A), d), relative_offsets(A, d))
170+
end
171+
```
110172
173+
Defining these two methods ensures that other array types that wrap `OffsetArray` and appropriately define these methods propagate offsets independent of any dependency on `OffsetArray`.
174+
It is entirely optional to define `ArrayInterface.size` for `OffsetArray` because the size can be derived from the axes.
175+
However, in this particularly case we should also define
176+
`ArrayInterface.size(A::OffsetArray) = ArrayInterface.size(parent(A))` because the relative offsets attached to `OffsetArray` do not change the size but may hide static sizes if using a relative offset that is defined with an `Int`.

src/ArrayInterface.jl

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ using Base: @propagate_inbounds, tail, OneTo, LogicalIndex, Slice, ReinterpretAr
1515

1616
const CanonicalInt = Union{Int,StaticInt}
1717

18-
@static if VERSION v"1.6.0-DEV.1581"
19-
_is_reshaped(::Type{ReinterpretArray{T,N,S,A,true}}) where {T,N,S,A} = true
20-
_is_reshaped(::Type{ReinterpretArray{T,N,S,A,false}}) where {T,N,S,A} = false
21-
else
22-
_is_reshaped(::Type{ReinterpretArray{T,N,S,A}}) where {T,N,S,A} = false
18+
@static if isdefined(Base, :ReshapedReinterpretArray)
19+
_is_reshaped(::Type{<:Base.ReshapedReinterpretArray}) = true
2320
end
21+
_is_reshaped(::Type{<:ReinterpretArray}) = false
2422

23+
@generated function merge_tuple_type(::Type{X}, ::Type{Y}) where {X<:Tuple,Y<:Tuple}
24+
Tuple{X.parameters..., Y.parameters...}
25+
end
2526
Base.@pure __parameterless_type(T) = Base.typename(T).wrapper
2627
parameterless_type(x) = parameterless_type(typeof(x))
2728
parameterless_type(x::Type) = __parameterless_type(x)
@@ -582,12 +583,15 @@ abstract type AbstractArray2{T,N} <: AbstractArray{T,N} end
582583
Base.size(A::AbstractArray2) = map(Int, ArrayInterface.size(A))
583584
Base.size(A::AbstractArray2, dim) = Int(ArrayInterface.size(A, dim))
584585

585-
Base.axes(A::AbstractArray2) = ArrayInterface.axes(A)
586+
function Base.axes(A::AbstractArray2)
587+
!(parent_type(A) <: typeof(A)) && return ArrayInterface.axes(parent(A))
588+
throw(ArgumentError("Subtypes of `AbstractArray2` must define an axes method"))
589+
end
586590
Base.axes(A::AbstractArray2, dim) = ArrayInterface.axes(A, dim)
587591

588592
function Base.strides(A::AbstractArray2)
589-
defines_strides(A) || throw(MethodError(Base.strides, (A,)))
590-
return map(Int, ArrayInterface.strides(A))
593+
defines_strides(A) && return map(Int, ArrayInterface.strides(A))
594+
throw(MethodError(Base.strides, (A,)))
591595
end
592596
Base.strides(A::AbstractArray2, dim) = Int(ArrayInterface.strides(A, dim))
593597

@@ -602,7 +606,7 @@ end
602606
function Base.length(A::AbstractArray2)
603607
len = known_length(A)
604608
if len === nothing
605-
return prod(size(A))
609+
return Int(prod(size(A)))
606610
else
607611
return Int(len)
608612
end
@@ -672,8 +676,6 @@ include("indexing.jl")
672676
include("stridelayout.jl")
673677
include("broadcast.jl")
674678

675-
676-
677679
function __init__()
678680

679681
@require SuiteSparse = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" begin
@@ -914,16 +916,46 @@ function __init__()
914916
end
915917
end
916918
@require OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" begin
917-
parent_type(::Type{O}) where {T,N,A<:AbstractArray{T,N},O<:OffsetArrays.OffsetArray{T,N,A}} = A
919+
relative_offsets(r::OffsetArrays.IdOffsetRange) = (getfield(r, :offset),)
920+
relative_offsets(A::OffsetArrays.OffsetArray) = getfield(A, :offsets)
921+
function relative_offsets(A::OffsetArrays.OffsetArray, ::StaticInt{dim}) where {dim}
922+
if dim > ndims(A)
923+
return static(0)
924+
else
925+
return getfield(relative_offsets(A), dim)
926+
end
927+
end
928+
function relative_offsets(A::OffsetArrays.OffsetArray, dim::Int)
929+
if dim > ndims(A)
930+
return 0
931+
else
932+
return getfield(relative_offsets(A), dim)
933+
end
934+
end
935+
ArrayInterface.parent_type(::Type{<:OffsetArrays.OffsetArray{T,N,A}}) where {T,N,A} = A
918936
function _offset_axis_type(::Type{T}, dim::StaticInt{D}) where {T,D}
919937
OffsetArrays.IdOffsetRange{Int,ArrayInterface.axes_types(T, dim)}
920938
end
921939
function ArrayInterface.axes_types(::Type{T}) where {T<:OffsetArrays.OffsetArray}
922940
Static.eachop_tuple(_offset_axis_type, Static.nstatic(Val(ndims(T))), ArrayInterface.parent_type(T))
923941
end
924-
@inline axes(A::OffsetArrays.OffsetArray) = Base.axes(A)
925-
@inline _axes(A::OffsetArrays.OffsetArray, dim::Integer) = Base.axes(A, dim)
926-
@inline axes(A::OffsetArrays.OffsetArray{T,N}, ::StaticInt{M}) where {T,M,N} = _axes(A, StaticInt{M}(), gt(StaticInt{M}(),StaticInt{N}()))
942+
function ArrayInterface.known_offsets(::Type{A}) where {A<:OffsetArrays.OffsetArray}
943+
ntuple(identity -> nothing, Val(ndims(A)))
944+
end
945+
function ArrayInterface.offsets(A::OffsetArrays.OffsetArray)
946+
map(+, ArrayInterface.offsets(parent(A)), relative_offsets(A))
947+
end
948+
@inline function ArrayInterface.offsets(A::OffsetArrays.OffsetArray, dim)
949+
d = ArrayInterface.to_dims(A, dim)
950+
ArrayInterface.offsets(parent(A), d) + relative_offsets(A, d)
951+
end
952+
@inline function ArrayInterface.axes(A::OffsetArrays.OffsetArray)
953+
map(OffsetArrays.IdOffsetRange, ArrayInterface.axes(parent(A)), relative_offsets(A))
954+
end
955+
@inline function ArrayInterface.axes(A::OffsetArrays.OffsetArray, dim)
956+
d = to_dims(A, dim)
957+
OffsetArrays.IdOffsetRange(ArrayInterface.axes(parent(A), d), relative_offsets(A, d))
958+
end
927959
end
928960
end
929961

0 commit comments

Comments
 (0)