Skip to content
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

Iterators.Reverse type for reverse-order iteration #24187

Merged
merged 2 commits into from Nov 2, 2017

Conversation

stevengj
Copy link
Member

@stevengj stevengj commented Oct 17, 2017

This PR closes #23972 by adding a new Reversed{T} Iterators.Reverse{T} wrapper type for reverse-order iteration.

The basic idea (suggested by @Keno) is that you can just loop over Iterators.reverse(iter) and it will go in reverse order, as long as start etcetera have been defined for Reverse{T}. Initially, I've implemented reverse iterators for AbstractArray, AbstractString, Tuple, Generator, and some other iterator types (zip, product, etc)..

I use Reversed and not Reverse because the latter name is already used for Base.Order.Reverse.

To do:

  • Tests
  • More documentation
  • Reverse-iteration support for other iterator types? (I've added most of the low-hanging fruit, I think. Other cases can go in separate PRs.)

@stevengj stevengj added the collections Data structures holding multiple items, e.g. sets label Oct 17, 2017
@ararslan ararslan added needs news A NEWS entry is required for this change needs tests Unit tests are required for this change labels Oct 17, 2017
@garrison
Copy link
Sponsor Member

garrison commented Oct 17, 2017

julia> Reversed(Reversed([1,2,3]))
Reversed{Reversed{Array{Int64,1}}}(Reversed{Array{Int64,1}}([1, 2, 3]))

Any reason not to return the original object instead?

EDIT: actually, the current result above cannot even be iterated over, throwing MethodError: no method matching start(::Reversed{Reversed{Array{Int64,1}}}).

Also, reverse(Reversed([1,2,3])) throws a MethodError: no method matching reverse(::Reversed{Array{Int64,1}}).

@ararslan
Copy link
Member

ararslan commented Oct 17, 2017

Should probably add definitions like

Reversed(r::Reversed) = r.iter
reverse(r::Reversed) = copy(r.iter)

Reversed(R::AbstractRange) = reverse(R) # copying ranges is cheap
Reversed(G::Generator) = Generator(G.f, Reversed(G.iter))
Reversed(r::Reversed) = r.iter
reverse(r::Reversed) = collect(r.iter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using collect is kind of weird for strings, isn't it? Or am I missing something?

@ararslan
Copy link
Member

Would it be worth adding something like

getindex(r::Reversed, i::Integer) = reverseind(r.iter, Int(i))

?

@stevengj
Copy link
Member Author

stevengj commented Oct 18, 2017

@ararslan, I thought about adding getindex, but I forgot about reverseind so I didn't see how to do it generically, and for arrays we can already use views for reverse-order indexing.

However, I'm still not sold on getindex. It's not that useful in this context for strings unless we also add nextind etcetera, and in general we don't usually define getindex for iterators.

@stevengj
Copy link
Member Author

I would have like to have used lower-case for the function, analogous to enumerate, zip, etcetera, but it is too confusing given that we already have a reverse function. Reverse-order iteration seems so basic, however, that I think it is worth exporting Reversed.

Any other bikeshedding on the name?

Reversed(R::AbstractRange) = reverse(R) # copying ranges is cheap
Reversed(G::Generator) = Generator(G.f, Reversed(G.itr))
Reversed(r::Reversed) = r.itr
reverse(r::Reversed) = collect(r.itr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question regarding collect. Seems odd for strings. (GitHub ate my previous comment.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this odd for strings? Reversed{<:String} is an iterator over characters, so should reverse(r) on it be the same as reverse(collect(r))?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected that since the underlying object being iterated is a string, unwrapping the Reversed wrapper would give you back the string, just as reverse(::String) gives you a string.

@ararslan
Copy link
Member

Calling it Reversed and exporting it seems okay to me.

@stevengj
Copy link
Member Author

Note: 44e383f is a bugfix that should be backported to 0.6. So, that commit should not be squashed with the others when/if this PR is merged.

@garrison
Copy link
Sponsor Member

I don't think calling it reversed would be so bad, might in fact be preferable to calling it Reversed.

@ararslan
Copy link
Member

I disagree; I think the difference between reverse and reversed is too subtle.

@ararslan ararslan removed the needs tests Unit tests are required for this change label Oct 18, 2017
@stevengj
Copy link
Member Author

Using caps does emphasize that it is a wrapper type (a “noun”) rather than a transformation of the underlying data (a “verb”).

Of course, one could similarly question why we don’t capitalize zip.

@ararslan
Copy link
Member

Of course, one could similarly question why we don’t capitalize zip.

In the very specific case of zip, the function returns a different type (Zip1, Zip2, or Zip) depending on the number of arguments. So it's a bit different from Reversed in that regard.

@nalimilan
Copy link
Member

We already have filter and Iterators.filter, the latter returning a Iterators.Filter iterator. Why not have reverse and Iterators.reverse, the latter returning an Iterators.Reverse iterator? That would be more consistent than reversed returning a Reversed iterator.

@stevengj
Copy link
Member Author

Note that we actually once did have a Reverse iterator, which was removed by @JeffBezanson for Julia 0.2 because it relied on getindex (#4590). Base.Order subsequently exported Reverse (#6780).

@stevengj
Copy link
Member Author

Probably we should have a reverse(x) = collect(Reversed(x)) fallback too.

@StefanKarpinski
Copy link
Sponsor Member

I think one generally wants a similar kind of collection when reversing, so that fallback might need some tweaking. In general, I suspect that having a general fallback for reversal is not worth it and we should just supply the methods where we know what to do and let people fill in the rest.

@stevengj stevengj removed the needs news A NEWS entry is required for this change label Oct 19, 2017
@stevengj
Copy link
Member Author

stevengj commented Oct 19, 2017

@StefanKarpinski, my objection is that it seems really weird and surprising for a collection to support Reversed but not reverse. (For example, Reverse supports n-dimensional arrays, but reverse does not, and neither does reverse(collect(a)).)

Imagine the user questions: "How do I reverse the order of this collection? reverse doesn't work, and neither does reverse(collect(x))" .... we answer: "Use collect(Reversed(x))" ... "Why wasn't this the default?" ... "Ummm...."

Another way of saying it: this reduces the number of methods a collection needs to implement. If reverse-order iteration is worthwhile, so you implement Reversed{T} methods, wouldn't you want reverse to work too?

@stevengj
Copy link
Member Author

stevengj commented Oct 19, 2017

@nalimilan's Iterators.reverse suggestion seems reasonable to me.

by iterating over `Reversed(iterator)`. [`Reversed`](@ref) is just a thin wrapper
around our collection; to actually support reverse-order iteration, an iterator
type `T` needs to implement `start`, `next`, and `done` methods for `Reversed{T}`.
(Given `r::Reversed{T}`, the underling iterator of type `T` is `Reversed(r)`.)
Copy link
Member Author

@stevengj stevengj Oct 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just document r.itr instead of suggesting Reversed(r) or Iterators.reverse(r) or whatever we decide to call it?

r.itr is a bit more transparent and convenient, but it does expose the internal field name.

@StefanKarpinski
Copy link
Sponsor Member

@stevengj, I'm willing to live with a few awkward questions about missing features in 1.0 rather than adding new, half-baked functionality that we then have to live with for a decade, no matter how wrong it turns out we are about how it behaves.

@stevengj
Copy link
Member Author

Okay, I removed the Base.reverse fallback, and renamed to Iterators.reverse and Iterators.Reverse.

@propagate_inbounds next(A::Reverse{<:AbstractArray}, i) = ((idx, s) = next(i[1], i[2]); (A.itr[idx], (i[1], s)))
@propagate_inbounds done(A::Reverse{<:AbstractArray}, i) = done(i[1], i[2])

Reverse(R::AbstractRange) = Base.reverse(R) # copying ranges is cheap
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of weird in that you're asking for a lazy reverse but getting a new range back instead of a Reverse iterator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ranges are already lazy, so it seems silly to define Reverse iteration methods for ranges.

But I guess I can now change this to a method of Iterators.reverse instead of Reverse.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I trust your judgement for whatever the best approach is here, just thought I'd point it out.

@@ -12,4 +12,6 @@ Base.Iterators.repeated
Base.Iterators.product
Base.Iterators.flatten
Base.Iterators.partition
Base.Iterators.filter
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some reason that Iterators.filter was not listed here previously? It seemed like an oversight.

Also, the ordering of methods here seems kind of random; is it automatically sorted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I think it was an oversight. Documenter doesn't do any sorting of methods—they appear in the output in the same order as you write them AFAIK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now that I look more closely at the order, it seems like they have been somewhat topically sorted, so I think the order is fine as-is.

@ararslan
Copy link
Member

Yeah, there have been a few issues with Circle, particularly x86-64, lately.

@stevengj
Copy link
Member Author

stevengj commented Oct 24, 2017

For comparison, the Python reversed iterator (PEP 322) only works for indexable collections. (We used to do the same thing, but removed it: #4590.) In Swift, the reversedcollection iterator only works for collections with "bidirectional indices", which is essentially equivalent to requiring getindex plus prevind and nextind. Ruby's reverse_each apparently only works on arrays (i.e. requires getindex). The only fully general lazy iterator reversal protocol I've come across in the "standard" libraries of other languages is Boost's bidirectional traversal concept and resulting reverse iterator.

@stevengj
Copy link
Member Author

Rebased to fix conflict.

Note: if/when this is merged, don't squash since the bugfix commit should be backported.

@ararslan ararslan added the triage This should be discussed on a triage call label Nov 2, 2017
@ararslan ararslan removed the triage This should be discussed on a triage call label Nov 2, 2017
@ararslan ararslan merged commit 68f1818 into JuliaLang:master Nov 2, 2017
@mauro3
Copy link
Contributor

mauro3 commented Nov 2, 2017

It looks like this was squashed, but wasn't this supposed to be not squashed?

@ararslan
Copy link
Member

ararslan commented Nov 2, 2017

It was not squashed.

@stevengj stevengj deleted the reverseiter branch November 2, 2017 19:00
@mauro3
Copy link
Contributor

mauro3 commented Nov 2, 2017

Ok, sorry for the noise!

ararslan pushed a commit that referenced this pull request Nov 7, 2017
ararslan pushed a commit that referenced this pull request Nov 8, 2017
ararslan pushed a commit that referenced this pull request Nov 8, 2017
ararslan pushed a commit that referenced this pull request Nov 8, 2017
ararslan pushed a commit that referenced this pull request Nov 8, 2017
ararslan pushed a commit that referenced this pull request Nov 9, 2017
ararslan pushed a commit that referenced this pull request Nov 14, 2017
ararslan pushed a commit that referenced this pull request Nov 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
collections Data structures holding multiple items, e.g. sets
Projects
None yet
Development

Successfully merging this pull request may close these issues.

reverse iteration protocol
8 participants