-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: support iteration for CartesianIndex
#48404
base: master
Are you sure you want to change the base?
Conversation
I'm almost certain this came up before, but wasn't able to find the discussion. I think now that the multidimensional indexing API has matured a bit, the potential for confusion when allowing iteration is excedingly low. With this change, it's possible to destructure `CartesianIndex` objects without having to convert them to a tuple, which I feel currently makes writing such code unnecessarily annoying. This also makes `CartesianIndex` broadcast like a scalar which I felt was consistent with the behavior for objects like `Pair`, but I am open to other suggestions.
Related: #47044 |
I'm favorable toward broadcasting as a scalar. Are there any Base objects that currently have such radically different broadcast vs iteration behavior as is proposed here? That's my initial concern. I don't mind the current cast-to-tuple requirement, as it's clear and doesn't (shouldn't?) actually impose any runtime cost. In my mind, a |
There's a bunch. I already mentioned |
Looks like a good idea. This discourse conversation discusses another potential benefit to making |
I think we have been down this road before: This is a duplicate of #23982 (RFC: make CartesianIndex iterable) See conversation in #23719 . Specifically, can we now avoid the performance trap? |
If I understand correctly, the performance trap doesn't exist on v1.9 julia> @inline Base.iterate(index::CartesianIndex) = iterate(index.I)
julia> @inline Base.iterate(index::CartesianIndex, st...) = iterate(index.I, st...)
julia> function mysum1(A)
s = 0.0
@inbounds for i in CartesianIndices(A)
s += A[i]
end
s
end
mysum1 (generic function with 1 method)
julia> function mysum2(A)
s = 0.0
@inbounds for i in CartesianIndices(A)
s += A[i...]
end
s
end
mysum2 (generic function with 1 method)
julia> using BenchmarkTools
julia> A = rand(10,10,10,10,10,10);
julia> @btime mysum1($A);
1.620 ms (0 allocations: 0 bytes)
julia> @btime mysum2($A);
1.541 ms (0 allocations: 0 bytes)
julia> mysum1(A) == mysum2(A)
true |
Do we know what changed to close the performance gap? Is there some situation where we might not have closed it? |
I think calling this a performance trap might have been a bit overblown. By now, I think people know how to use |
The passing of time does not solve the issue for new users to Julia. However, given the current benchmarks of the original example, I concur that the original issues appears to be moot. Perhaps we could invite @timholy to present a new challenge? |
I seem to recall that @mbauman had the clearest examples of the problems of making |
This was back in #21148 — which I closed as we finalized the new broadcasting semantics that defaulted to iteration with very few exceptions. I think the only thing that was iterable but broadcasted like a scalar at that time was String... and I was very hesitant to add others like it (I was deep in the weeds of #18618.). Since then we've added So there are two questions here:
Now that my head is back in #18618-land, I'm also thinking that there's a third question here:
|
This is my main concern too, and I confess a hint of uneasiness. |
My default would be to throw an error for now and reconsider in another PR since there is uncertainty. |
IMO, being unable to destructure and splat a CartesianIndex is just so ugly and annoying that I'd be strongly in favour of having another weird object that is a broadcast scalar yet iterable. |
@MasonProtter I'm sure you know about |
I go between |
The main reason I find this annoying is that I often find myself wanting to write code similar to the following: for (i, j) in CartesianIndices(M)
if abs(i) + abs(j) ≤ N
# do something
end
end Currently, you would have to write this as: for I in CartesianIndices(M)
(i, j) = Tuple(I)
if abs(i) + abs(j) ≤ N
# do something
end
end It's not the end of the world of course, but I just found it quite unergonomical. With the support for slurping added here, this is even more powerful, e.g. to contract an n-1-dimensional array with a vector you could simply write: for (I..., j) in CartesianIndices(B)
A[I] += B[I, j] * v[j]
end With the current state of things, this would involve converting back and forth between |
I propose another solution: decouple unpacking from iteration. This solution doesn't proliferate the strange iterable-but-scalar objects but retains the syntactic convenience of |
Great example @simeonschaub, that's exactly the sort of thing I've also encountered. |
|
Julia can introduce Introducing types where |
There is a long list of such types, so I guess we have to live with this discrepancy. @simeonschaub - my question would be in what cases do we need to broadcast It is easy enough to use The point is that It is not clear (at least for me) if it is better to make:
or
work. |
Leaving out the broadcasting part means this will use the generic fallback and collect before broadcasting, so either way we have to make a decision now – we can't change this behavior later. If DataFrames wants to mimick broadcasting behavior, why doesn't it just call |
I proposed to disallow broadcasting, just like broadcasting is disallowed for
DataFrames.jl does not want to mimick broadcasting. (my comment above is unrelated to DataFrames.jl; for those interested let me just comment that "scalar" is something different than size-1 broadcastable object in the context of DataFrames.jl; a similar thing is in linear algebra you allow multiplication by a scalar, but do not allow multiplication by 1-element vector) |
This, at least, we have a reasonably nice idiom for: |
I don't think it can make sense to broadcast as anything other than a scalar. a = [1; 2;; 3; 4]
b = [5; 6;; 7; 8]
c = getindex.((a,b), CartesianIndex(1,2))
# c == (1, 6) if CartesianIndex broadcasts as collection
# c == (3, 7) if CartesianIndex broadcasts as scalar
d = getindex.((a,b,b), CartesianIndex(1,2))
# error if CartesianIndex broadcasts as collection
# d == (3, 7, 7) if CartesianIndex broadcasts as scalar |
The ship has sailed on how we broadcast CartesianIndex — #47044 is now merged but prior to that we've long had #29890. So the only question is iterability.
Folks keep saying this, but at least within the standard library there is only |
And these two are constantly problematic + add BTW: in DataFrames.jl |
To slightly digress, I think we could also partly justify it by noting that |
Triage agrees with @mbauman as to what the potential issues are, but thinks that this is still a relatively reasonable choice to make and leaves the final decision with him. |
The way I usually write code like that is either like
or
because it retains the ability to extend the code easily to higher dimensions. From my POV, CartesianIndex is a really nice analog to a vector in an N-dimensional integer vectorspace and indexing those by single dimension doesn't really make sense to me. I don't have particularly strong feeling about it though, just that I personally probably wouldn't use this. |
This seems more readily extensible to higher dimensions: for I in CartesianIndices(M)
if sum(abs, I) ≤ N
# do something
end
end but that would require |
That's not the same function anymore; the original looked at a 2D cube (rectangle) and did some processing based on that, while yours looks at a N-D cube (which doesn't ignore singleton dimensions). May be the correct choice in some situations, but it doesn't generalize cleanly - the original version ignores the additional dimensions after all, selecting every index in a third extended dimension. That's why I prefer the explicitness. |
The example wasn't clear in which manner extensibility would be desired, so I picked what I thought was meant. In any case, @mkitti's idea I posted above was to allow functions to be array initializers with a Array(m, n, o) do (i, j, k)
i + j + k
end If |
I'm almost certain this came up before, but wasn't able to find the
discussion. I think now that the multidimensional indexing API has
matured a bit, the potential for confusion when allowing iteration is
excedingly low. With this change, it's possible to destructure
CartesianIndex
objects without having to convert them to a tuple,which I feel currently makes writing such code unnecessarily annoying.
This also makes
CartesianIndex
broadcast like a scalar which I feltwas consistent with the behavior for objects like
Pair
, but I am opento other suggestions.