I’ve now seen a couple of cases where folks seem unhappy about the removal of the functions
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
sub2ind. In Julia 0.7 these were replaced by objects created by
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
i2sonly 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 using
sub2indevery day, then I’ll wager that you have to remind yourself whether it’s
ind2sub(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 (
i2sabove) 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 had
CartesianIndices(it used to be called
CartesianRange) for iterating over arrays; all we had to do was endow it with new properties, at which point it subsumed the purpose of
ind2sub. 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)
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
In summary, the new API is memorable and efficient, and consequently recommended for general use.