`view` and `[ ]` with "slice object"

A tuple is often used to encapsulate a set of array sizes: sz = size(multi_dim_arr) and use sz later. In a similar manner, I sometimes want to encapsulate an array “slice” in an object, but I’m wondering what’s the best way.

arr = rand(Float64, 3, 5)
s = size(arr) # -> Tuple (3,5)
b = fill("hello", s) # works
b = fill("hello", 3, 5) # works, too.
slice = ( :, 2)
c = view(arr,  slice) # error
c = view(arr, slice...) # works
d = arr[slice...]  # works, too.
slice2 = CartesianIndices((:, 2)) # error
slice2 = CartesianIndices((1:3, 2)) # fine
e = arr[slice2] # works

As you can see, there isn’t quite a “slice object” that can express a general slice. CartesianIndices comes close but it can’t include a Colon-only range.

It would be nice if view and [ ] accepted a slice object that can be used like

function func(sl2d)
   # build a 4D array
   t = view(arr4d, sl2d)
   u = arr[sl2d]
   func_on_2d_arr(t) #  or u
end
sl = Slice(4,  :,  3,  2:9)
func(sl)

So, my question is whether there is an idiomatic way to encapsulate a slice in a variable (object) and use it later. Is v[tuple...] the way to go? Or would it make sense to extend view and [ ] to take a slice object (as represented by a tuple, for example)?

It seems that your proposed Slice object is a reasonable way to do this (although be careful to avoid mixing it up with Base.Slices).

An extremely simple implementation might look like this

struct ArraySlice{T<:Tuple}
	s::T
end

ArraySlice(s...) = ArraySlice(s) # Vararg constructor

Base.getindex(x::AbstractArray, slice::ArraySlice) = Base.getindex(x, slice.s...)
Base.view(x::AbstractArray, slice::ArraySlice) = Base.view(x, slice.s...)

It’s important that a specialized object was used for this (which you appear to suggest already, but I’m emphasizing it here). Do not simply overload Base.getindex(x::AbstractArray, slice::Tuple) so that tuples are interpreted like your slice object. That would be type piracy and would have the potential to break things spectacularly if some other code (that you write or load, or that something you load loads) were to try a similar overload.


Another option, if you have the object (or its indices) known to you in advance so that you can resolve :, begin, and end immediately, is to slice the CartesianIndices of your target object

julia> a = zeros(2,3,4);

julia> sl = CartesianIndices(a)[:,3:-1:1,4]
2×3 Matrix{CartesianIndex{3}}:
 CartesianIndex(1, 3, 4)  CartesianIndex(1, 2, 4)  CartesianIndex(1, 1, 4)
 CartesianIndex(2, 3, 4)  CartesianIndex(2, 2, 4)  CartesianIndex(2, 1, 4)

julia> sl = @view CartesianIndices(a)[:,3:-1:1,4]
2×3 view(::CartesianIndices{3, Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}, Base.OneTo{Int64}}}, :, 3:-1:1, 4) with eltype CartesianIndex{3}:
 CartesianIndex(1, 3, 4)  CartesianIndex(1, 2, 4)  CartesianIndex(1, 1, 4)
 CartesianIndex(2, 3, 4)  CartesianIndex(2, 2, 4)  CartesianIndex(2, 1, 4)

Note that the getindex version of this is producing an Array (Matrix, in this case), which will allocate. The view version should not, so is probably preferable. Note that you don’t need to pass a, but can instead use CartesianIndices(axes(a)) if you only know the axes of your target. You can even go so far as to just use the size, although that is fragile because it may break for OffsetArrays or other non-1-indexed arrays.

1 Like

Colons (and other unusual indexing things) get converted upon indexing/view using to_indices. CartesianIndices don’t support : because they don’t have the context about the array they’ll eventually be used in. You can give them that context:

julia> arr = rand(Float64, 3, 5);

julia> slice = ( :, 2)
(Colon(), 2)

julia> slice2 = CartesianIndices(to_indices(arr, slice))
CartesianIndices((1:3, 2))

Overloading indexing like @mikmoore suggests is going to be a huge headache of ambiguities, I would think. You could overload to_indices(arr, inds, S::ArraySlice) if you wanted to index with such an object without splatting. But I’d just splat the tuple.

2 Likes

Thank you folks for your detailed, well-informed discussion! I now know how or in which way I should proceed.

In the meanwhile, as I write small programs, I find myself wanting to write

slice = (1:10:end) # strided indexing
. . .
func(v[slice], . . .)
another_func(arr[slice], . . .)

which doesn’t work because end isn’t known in that context.

Writing in Fortran, I used to have a lot of little branches. In Julia, most of such branches are gone because you can encapsulate various information in variables (objects). Array slicing is probably the last thing (to me) for which there is no idiomatic or obvious solution.

Perhaps GitHub - JuliaArrays/EndpointRanges.jl: Julia package for doing arithmetic on endpoints in array indexing might help with this?

1 Like

Yes! Thank you for the information!

With that package, I can fulfill all my needs:

using EndpointRanges

a1 = 1:11
a3 = reshape(1:(3*2*4), 3, 2, 4)

s1 = (ibegin:3:iend, ) # 1D slice object
s3 = (2, :, ibegin:3:iend) # 3D slice object

b1 = a1[s1...] # get the slice
b3 = a3[s3...] # get the slice

display(collect(b1))
display(collect(b3))

With this, one could modify Base.getindex and Base.view as suggested by @mikmoore .