-
-
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
Added tests for Iterators to cover a few edge cases. #35916
Conversation
@@ -100,6 +100,7 @@ end | |||
@test length(zip(cycle(1:3), 1:7, cycle(1:3))) == 7 | |||
@test length(zip(1:3,product(1:7,cycle(1:3)))) == 3 | |||
@test length(zip(1:3,product(1:7,cycle(1:3)),8)) == 1 | |||
@test_throws ArgumentError length(zip()) # length of zip of empty tuple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, length
should be extended to return 0 in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think length
of empty zip
should be infinite. This PR also tests that Base.IteratorSize(zip()) == Base.IsInfinite()
holds. Also, for x in zip(); end
is an infinite loop (which, I think, is correct).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should zip()
be infinite?
If I collect/group/zip elements from no collections, I don’t expect there to be any elements, i.e. isempty(collect(zip())) == isempty(zip()) == true
. Just like I would expect zip(xs...) = map(Tuple, xs...)
and that should have the length of the shortest iterable among xs...
, which I think should represent the number of elements in a collection. If there are no collections, there no elements, hence its length should be zero.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since length(zip(itrs...)) == minimum(length, itrs) == mapreduce(length, min, itrs)
, the value length(zip())
should be the identity element of min
. The identity element of min
on bounded set is the maximum of the set (it has to "loose" against everything else when taking min
). Since the output of length
can take any non-negative integer, "infinite" is a reasonable answer.
Another way to think about is that, if you think the mapping (a, b) -> zip(a, b)
as a monoid and define zip(itrs...) = reduce(zip, itrs)
[*1], the iterator zip()
should be the identity of the monoid. An infinite stream of empty tuples is the identity element of the zip
monoid. The length of an infinite stream is, well, infinite.
[*1] You have to "post-process" to flatten the output tuple. But other than that, this monoid argument is accurate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yet another way to convince yourself is to consider a function
function zip′(itrs...)
a, b = anysplit(itrs)
return ((x..., y...) for (x, y) in zip(zip(a...), zip(b...)))
end
with a function anysplit
that splits a tuple at some location such that
ys, zs = anysplit(xs::Tuple)
@assert (ys..., zs...) === xs
It is desirable for the function zip′
to behave like zip
for any anysplit
function.
In particular, for this to hold for anysplit(xs) = (), xs
, we need zip()
to produce an infinite stream of empty tuples.
(Edit: fix a typo in anysplit
signature)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation! The monoid view was helpful. I agree that the stream of empty tuples would be the neutral element, if we consider equivalence under "generates the same elements". Though it's not intuitive to me why zip()
would be repeated(())
.
I know that the following is currently wrong in Julia, but as we already consider the empty tuple to be identical to "no element", I would expect this:
using Test
@testset "empty collections" begin
null = Vector{Union{}}()
@test collect(zip(())) == null
@test collect(zip()) == null # fails
@test collect(()) == null
@test collect() == null # fails
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand, having a zip
name for the zip
-neutral element kinda makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's as counter-intuitive as the neutral element of min
being the maximum. Or maybe more like the intersection of empty set being the universe. I agree it feels like a paradox but "fixing it" would break mathematical coherence behind the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM other than the minor nitpicks I mentioned.
Minor fixes. Co-authored-by: Takafumi Arakaki <[email protected]>
Hi everyone, |
Improves test coverage for Iterators.
Covers a few edge cases.