I’ve now seen a couple of cases where folks seem unhappy about the removal of the functions ind2sub
and sub2ind
. Some of the concerns may stem from the suggested replacement in the deprecation warning, as the replacement, while “safe,” seems considerably more awkward. This is a short post explaining their replacements and the motivation for the change; once you learn to think in the new way, I’d be surprised if you don’t become a fan of the new approach.
In older versions of Julia, the way to convert between a linear index and a cartesian index was to use the conversion functions ind2sub
and sub2ind
. In Julia 0.7 these were replaced by objects created by CartesianIndices
and LinearIndices
; for example,
function oldi2s(a, i)
sz = size(a)
ind2sub(sz, i)
end
should now be written
function newi2s(a, i)
i2s = CartesianIndices(a)
i2s[i]
end
There are two concerns I’ve heard. I’ll address them both.
-
That’s a lot of characters. Why the more verbose version? It’s only more verbose if you use
i2s
only once; if you do conversions in multiple places in the same function, the new syntax is considerably shorter. But more importantly, the old API has a poor design because it suffers from “must read the docs” syndrome: if you’re not usingind2sub
andsub2ind
every day, then I’ll wager that you have to remind yourself whether it’sind2sub(sz, i)
orind2sub(i, sz)
—it’s pretty much an arbitrary choice, and as a consequence infrequent users routinely have to check the docs every time they use them.
The new API solves that uncertainty by splitting it into two steps: (a) creation of the conversion object (i2s
above) depends only on the array, and (2) calculation of the index depends only on the index. Since each operation stands alone, there’s no order confusion.
The final nice thing about the new API is that we already hadCartesianIndices
(it used to be calledCartesianRange
) for iterating over arrays; all we had to do was endow it with new properties, at which point it subsumed the purpose ofind2sub
. There seems little reason to keep an entirely redundant function. - Isn’t it slow to create these objects? No, it’s not. Here’s a demo:
julia> function newi2s(a, i)
i2s = CartesianIndices(a)
i2s[i]
end
newi2s (generic function with 1 method)
julia> a = rand(5, 7);
julia> using BenchmarkTools
julia> newi2s(a, 12)
CartesianIndex(2, 3)
julia> @btime newi2s($a, 12)
6.379 ns (0 allocations: 0 bytes)
CartesianIndex(2, 3)
Compare 0.6:
julia> function oldi2s(a, i)
sz = size(a)
ind2sub(sz, i)
end
oldi2s (generic function with 1 method)
julia> @btime oldi2s($a, 12)
15.065 ns (0 allocations: 0 bytes)
(2, 3)
So Julia 1.0 still has no allocations, and is 2x faster than Julia-0.6. If you know the access is in-bounds you can make it even faster by changing the relevant line to @inbounds i2s[i]
.
In summary, the new API is memorable and efficient, and consequently recommended for general use.