Understanding `view`


#1

I’m trying to figure out how to use view… Consider this simple example:

x = [1, [1 2; 3 4]]
y = view(x,1)
z = view(x,2)

Here, I would have guessed that y = 1 and z = [1 2; 3 4], and that I could address element (2,1) of z by: z[2,1].

This doesn’t work, though. Instead, I have to use the syntax z[1][2,1].

Why?..

julia> x = [1, [1 2;3 4]]
2-element Array{Any,1}:
 1
  [1 2; 3 4]

julia> y = view(x,1)
0-dimensional view(::Array{Any,1}, 1) with eltype Any:
1

julia> z = view(x,2)
0-dimensional view(::Array{Any,1}, 2) with eltype Any:
[1 2; 3 4]

julia> z[2,1]
ERROR: BoundsError: attempt to access 0-dimensional view(::Array{Any,1}, 2) with eltype Any at index [2, 1]
Stacktrace:
 [1] throw_boundserror(::SubArray{Any,0,Array{Any,1},Tuple{Int64},false}, ::Tuple{Int64,Int64}) at .\abstractarray.jl:484
 [2] checkbounds at .\abstractarray.jl:449 [inlined]
 [3] _getindex at .\abstractarray.jl:939 [inlined]
 [4] getindex(::SubArray{Any,0,Array{Any,1},Tuple{Int64},false}, ::Int64, ::Int64) at .\abstractarray.jl:905
 [5] top-level scope at none:0

julia> z[1][2,1]
3

#2

view is returning an array-like object so you can obtain slices of your vector or matrix. For example,

julia> x = [1, [1 2; 3 4], "hi"]
3-element Array{Any,1}:
 1          
  [1 2; 3 4]
  "hi"      

julia> z = view(x,2:3)
2-element view(::Array{Any,1}, 2:3) with eltype Any:
 [1 2; 3 4]
 "hi"      

julia> z[1]
2×2 Array{Int64,2}:
 1  2
 3  4

julia> z[2]
"hi"

So you need to use z[1] because you are essentially creating array-like objects with one element for each of your examples.


#3

I do find it a bit confusing that passing a scalar argument to view returns a container particularly considering that

@view x[1]

is valid syntax (that returns a container). I imagine there’s a good reason for why it is the way it is, but I’m a bit fuzzy on that at the moment.


#4

Yes, view and getindex differ in their behaviors here: getindex (that is, the A[i] syntax) with scalar indices will return the object at that location. On the other hand, view is expressly saying “I want to get a view into a chunk of this array that will always reflect the contents in that location.” This means that you can always modify a view and have its changes propagate back to the original array — and vice versa.

This becomes more clear with immutable elements:

julia> A = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> V = view(A, 2)
0-dimensional view(::Array{Int64,1}, 2) with eltype Int64:
2

julia> V[] = 4
4

julia> A
3-element Array{Int64,1}:
 1
 4
 3

julia> A[2] = 5
5

julia> V
0-dimensional view(::Array{Int64,1}, 2) with eltype Int64:
5

If it unwrapped that returned element, then, well, it wouldn’t be a view anymore and changes in A would not propagate to V. While @view is just the same as calling view instead of getindex, we also provide @views that does the kind of element-unwrapping for the scalar case to work more akin to getindex.

In this respect, view is the sane function — it always returns a view into the array. getindex is doing the old “sometimes scalar, sometimes nonscalar” kinds of operations that we used to do before we gained the dot-syntax. It’s perhaps the last remaining function that does so.


#5

I am not sure whether you are implying that getindex is not “sane” — its semantics are well-defined and also pretty standard in languages that use arrays (except for R, which has no scalars).

It’s just that view and getindex are different things with different semantics. Perhaps the fact that @view is using the syntax of [] is confusing.


#6

I just mean that view has one behavior whereas getindex has two different modes of operation. That fact has made a potential corresponding broadcasted getindex for fusion quite confusing. There are some challenges with well-ingrained idioms, but there’d be advantages to leaning on broadcasting to provide the nonscalar behaviors — that’s its reason for being!


#7

Can you please elaborate? My understanding is that the role of the non-scalar getindex is to provide slices, eg A[2:3, 4:(end-1)], which can then be broadcasted. The way I think about these two steps is that they are conceptually orthogonal (even if optimizations can make use of combining them).


#8

The role of non-scalar getindex is to provide a structured way to repeatedly call the scalar getindex. We don’t think of it as a repeated access (it’s just a slice!), but it’s really just going through the passed arrays of indices and calling the scalar method at each index. That’s what broadcast does at its core, too — it repeatedly calls some scalar function across array(s) of arguments.

The huge advantage to using broadcast to represent nonscalar indexing is that it could fuse with other broadcasted operations. Forget the whole view/copy debate — fusion would be faster than either. For example, we could define the new syntax A.[I...] to mean broadcast((idxs...)->A[idxs...], I...). Then, to extract the first 10 elements from the first column of A, you write A.[1:10, 1]. This means that you could fuse in additional operations without any temporaries or views whatsoever with, e.g., sqrt.(A.[1:10, 1])

The other huge advantage is how this would then generalize these sorts of accesses to all data structures, including dictionaries and more.

There are key differences in how broadcast behaves vs. non-scalar indexing. The biggest of course being how multiple array arguments are combined: indexing takes the cartesian product of its arguments to form our dim-sum rule, whereas broadcasting combines dimensions. For example, A.[1:10, 1:10] would return the first ten elements on the diagonal, whereas A[1:10, 1:10] returns the whole 10x10 rectangle. You can think of nonscalar indexing as doing a broadcast where each successive argument is “lifted” to a dimension orthogonal to all previous arguments. I’ve thought about introducing the operator to represent this. E.g., f.(1:10, ⟂(1:10)) would evaluate f over the cartesian product. It would resolve much of my own abuse of x'.

There are other issues, though, including :s, logical indexing, fusion with index computations, bounds checking, and more. So that’s why it’s just an aside in my above post and not a reality. At least not yet. https://github.com/JuliaLang/julia/issues/19169#issuecomment-311186626


#9

Thanks, it is now clear. I appreciate the detailed explanation.