Why does using keys() on an array produce LinearIndices?

Suppose I had the following array and used keys() to extract the indices:

trance_singers = String["Emma Hewitt","Carrie skipper","Justine Suissa"]
println(typeof(keys(singers))) # LinearIndices{1,Tuple{Base.OneTo{Int32}}}

Why not just a Int32[]? I’m guessing, but to ensure that the first array element is in fact 1, it uses Base.OneTo(), but why does it need to be inside a tuple inside LinearIndices?

my guess is for memory efficiency, e.g.:

julia> a=rand(3)
3-element Array{Float64,1}:
 0.8118668996310585
 0.9347418652287747
 0.3923256153519141

julia> sizeof(keys(a))
8

julia> a=rand(1000000000);

julia> sizeof(keys(a))
8

In the last case an Int[…] would be of size 8000000000:

julia> typeof(collect(keys(a)))
Array{Int64,1}

julia> sizeof(collect(keys(a)))
8000000000
julia> A = fill(1, (5,6,7));

julia> b = LinearIndices(A);

julia> typeof(b)
LinearIndices{3,Tuple{Base.OneTo{Int64},Base.OneTo{Int64},Base.OneTo{Int64}}}

It is inside a Tuple of a single element because it has a single dimension, if it had more dimensions, the tuple would have the same length as the number of dimensions.

It doesn’t matter what the first index is actually:

julia> using OffsetArrays

julia> a = rand(5);

julia> keys(OffsetArray(a, -2))
5-element LinearIndices{1,Tuple{OffsetArrays.IdOffsetRange{Int64,Base.OneTo{Int64}}}} with indices -1:3:
 -1
  0
  1
  2
  3

but why does it need to be inside a tuple inside LinearIndices ?

It needs to be inside a tuple to make as much type information available as possible. The tuple is in the LinearIndices so that methods can be written specifically for the LinearIndices type (basically to provide “semantics” to the tuple.) CartesianIndices has the same underlying tuple but different implementations of (some of) the same functions.

In addition to the answers above: I would just treat the internals of LinearIndices as an implementation detail. The main point is that it is an object on which you can iterate.

Actually, in this context even LinearIndices itself is an implementation detail. It’s just something that keys returns.

7 Likes

Seeing as you brought up implementation details, I was wondering if you could clarify something for me. I asked a question about keyword arguments and how they’re stored, but I wasn’t really satisfied with the answer, so I was hoping to get a second opinion.

When a structure (NamedTuple,LinearIndices,etc) is considered an implementations detail, is to okay to use that structure?

In the case of keyword arguments, it’s really a Iterator.Pairs() holding a NamedTuple, and the documentation clearly states it. You can use values() to extract the NameTuple and access the elements by keywords. However, does relying on it being a NamedTuple make the code fragile? I’m assuming if it does change in the future, the documentation would be updated to reflect that, and even if it isn’t, you can call typeof() to view the type of structure. Would it be better to create your own NamedTuple as demonstrated in the linked answer?

In the context of LinearIndices, should I just rely on the fact I have an iterable, rather than a more specific structure?

Relying specifically on a NamedTuple would probably be fragile. Relying on a thing that behaves like a NamedTuple is certainly safe. IMO, that is also what the reply to the other thread is demonstrating.

1 Like

This depends on how it was created. Eg if you obtained a LinearIndices with keys, all you can rely on is what keys promises (an iterator or collection). However, if you created a LinearIndices explicitly, you can assume it is one.

Similarly for NamedTuples, it depends on how you got them. In Julia, APIs are usually organized primarily around methods, not types.

4 Likes