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

RFC: Add Stateful iterator wrapper #25731

Merged
merged 1 commit into from
Jan 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -765,3 +765,7 @@ Indicate whether `x` is [`missing`](@ref).
"""
ismissing(::Any) = false
ismissing(::Missing) = true

function popfirst! end
Copy link
Member

Choose a reason for hiding this comment

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

Do we still need this line?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, this generally gets loaded quite early.

function peek end

104 changes: 103 additions & 1 deletion base/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import .Base:
isempty, length, size, axes, ndims,
eltype, IteratorSize, IteratorEltype,
haskey, keys, values, pairs,
getindex, setindex!, get
getindex, setindex!, get, popfirst!,
peek

export enumerate, zip, rest, countfrom, take, drop, cycle, repeated, product, flatten, partition

Expand Down Expand Up @@ -958,4 +959,105 @@ function next(itr::PartitionIterator, state)
return resize!(v, i), state
end

"""
Stateful(itr)

There are several different ways to think about this iterator wrapper:
1. It provides a mutable wrapper around an iterator and
its iteration state.
2. It turns an iterator-like abstraction into a Channel-like
abstraction.
3. It's an iterator that mutates to become its own rest iterator
whenever an item is produced.

`Stateful` provides the regular iterator interface. Like other mutable iterators
(e.g. `Channel`), if iteration is stopped early (e.g. by a `break` in a `for` loop),
iteration can be resumed from the same spot by continuing to iterate over the
same iterator object (in contrast, an immutable iterator would restart from the
beginning).

# Example:
```jldoctest
julia> a = Iterators.Stateful("abcdef");

julia> isempty(a)
false

julia> popfirst!(a)
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

julia> collect(Iterators.take(a, 3))
3-element Array{Char,1}:
'b'
'c'
'd'

julia> collect(a)
2-element Array{Char,1}:
'e'
'f'
```

```jldoctest
julia> a = Iterators.Stateful([1,1,1,2,3,4]);

julia> for x in a; x == 1 || break; end

julia> Base.peek(a)
3
Copy link
Member

Choose a reason for hiding this comment

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

why not overloads first, like zip does?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair point, that would be consistent, though the generic definition of first iterates, so people might expect it to consume an item (that doesn't really bother me too much though, since take! is the consume-one-item function). first is defined to error on an empty collection though, I find returning nothing a more useful behavior, but I guess peek on IO doesn't really do that either. Maybe we should have a first equivalent that returns nothing or another sentinel when the collection is empty? @nalimilan ?

Copy link
Member

@iblislin iblislin Jan 25, 2018

Choose a reason for hiding this comment

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

Fair point, that would be consistent, though the generic definition of first iterates, so people might expect it to consume an item.

I don't think first will consume item given that it's not first!.

first is defined to error on an empty collection though

We already have something breaks that rule.

julia> first(1:0)
1

Copy link
Member

Choose a reason for hiding this comment

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

Changing the state of stateful iterators and doing I/O has never qualified as mutation in the sense that ! at the end of a name indicates. Otherwise all of the I/O functions would end in ! yet they do not. We need to decide if first qualifies as iteration or not. Having peek as a non-iterating form of take seems reasonable to me, but then so would first.

Copy link
Member

Choose a reason for hiding this comment

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

I'd also expect first to consume elements, so I think I prefer adding peek. We could generalize it so that it returns nothing for empty collections as @Keno suggests. Also, it sounds like one function is redundant in the first/peek/take! triad, and I'd tend to drop take! in favor of first.

Regarding the empty ranges issue at #25385, I guess we could make first return nothing in general for empty collections, but I'm a bit concerned about the performance implications.

Copy link
Member

Choose a reason for hiding this comment

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

It's also arguable that take! for channels should not have a ! since it is like read whereas take!(::IOBuffer) takes ownership of the data, which is why it has a !, so this is a bit muddled. By that reasoning, take!(::Channel) should be first(::Channel) but that's kind of unintuitive.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could use popfirst! instead of take! for the mutating operation. I still like peek rather than first, because the latter feels like a bit of a pun.


# Sum the remaining elements
julia> sum(a)
7
````
"""
mutable struct Stateful{T, VS}
itr::T
# A bit awkward right now, but adapted to the new iteration protocol
nextvalstate::Union{VS, Nothing}
taken::Int
# Try to find an appropriate type for the (value, state tuple),
# by doing a recursive unrolling of the iteration protocol up to
# fixpoint.
function fixpoint_iter_type(itrT::Type, valT::Type, stateT::Type)
nextvalstate = Base._return_type(next, Tuple{itrT, stateT})
nextvalstate <: Tuple{Any, Any} || return Any
nextvalstate = Tuple{
typejoin(valT, fieldtype(nextvalstate, 1)),
typejoin(stateT, fieldtype(nextvalstate, 2))}
return (Tuple{valT, stateT} == nextvalstate ? nextvalstate :
fixpoint_iter_type(itrT,
fieldtype(nextvalstate, 1),
fieldtype(nextvalstate, 2)))
end
function Stateful(itr::T) where {T}
state = start(itr)
VS = fixpoint_iter_type(T, Union{}, typeof(state))
vs = done(itr, state) ? nothing : next(itr, state)::VS
new{T, VS}(itr, vs, 0)
end
end

convert(::Type{Stateful}, itr) = Stateful(itr)

isempty(s::Stateful) = s.nextvalstate === nothing

function popfirst!(s::Stateful)
isempty(s) && throw(EOFError())
val, state = s.nextvalstate
s.nextvalstate = done(s.itr, state) ? nothing : next(s.itr, state)
s.taken += 1
val
end

peek(s::Stateful, sentinel=nothing) = s.nextvalstate !== nothing ? s.nextvalstate[1] : sentinel
start(s::Stateful) = nothing
next(s::Stateful, state) = popfirst!(s), nothing
done(s::Stateful, state) = isempty(s)
IteratorSize(::Type{Stateful{VS,T}} where VS) where {T} =
isa(IteratorSize(T), SizeUnknown) ? SizeUnknown() : HasLength()
eltype(::Type{Stateful{VS, T}} where VS) where {T} = eltype(T)
IteratorEltype(::Type{Stateful{VS,T}} where VS) where {T} = IteratorEltype(T)
length(s::Stateful) = length(s.itr) - s.taken

end
14 changes: 14 additions & 0 deletions test/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,17 @@ end
@test Iterators.reverse(Iterators.reverse(t)) === t
end
end

@testset "Iterators.Stateful" begin
let a = Iterators.Stateful("abcdef")
@test !isempty(a)
@test popfirst!(a) == 'a'
@test collect(Iterators.take(a, 3)) == ['b','c','d']
@test collect(a) == ['e', 'f']
end
let a = Iterators.Stateful([1, 1, 1, 2, 3, 4])
for x in a; x == 1 || break; end
@test Base.peek(a) == 3
@test sum(a) == 7
end
end