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

Unfold() is an iterable based on a transition function #44873

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ff456dd
IterableClosure is an iterator based on a closure
nlw0 Apr 6, 2022
f7e6dcd
Greatly improved implementation mostly by @Seelengrab
nlw0 Apr 7, 2022
f82e527
Update iterators.jl
nlw0 Apr 8, 2022
a097683
Update iterators.jl
nlw0 Apr 8, 2022
81bf243
one-liner iterate
nlw0 Apr 8, 2022
ba1c5fa
Update base/iterators.jl
nlw0 Apr 9, 2022
a1dc72e
Update base/iterators.jl
nlw0 Apr 9, 2022
6c68372
Update base/iterators.jl
nlw0 Apr 9, 2022
751dd4e
Update base/iterators.jl
nlw0 Apr 9, 2022
88dfb1e
rename plus slight eltype correction
nlw0 Apr 9, 2022
75c729b
docstring
nlw0 Apr 9, 2022
6df1cbe
some Unfold tests
nlw0 Apr 9, 2022
6ddf28e
Unfold range test
nlw0 Apr 9, 2022
9fcb440
doc, news
nlw0 Apr 9, 2022
24e9d85
Unfold tree test
nlw0 Apr 9, 2022
bdc531b
Update test/iterators.jl
nlw0 Apr 9, 2022
96a2fdf
replaced a couple of tests
nlw0 Apr 11, 2022
bf03b47
type specifications seem to have caused a problem
nlw0 Jul 8, 2022
0847d9f
removing initial state from struct, separate unfold function based on…
nlw0 Jul 9, 2022
5b782d5
reference to previous PR on NEWS.md
nlw0 Jul 9, 2022
62cfe4f
docs fix and removing mysteryous lingering definition
nlw0 Jul 9, 2022
1ba74ea
exporting and slight docstring changes
nlw0 Jul 9, 2022
e29ff52
default eltype thing didn't work, making it two separate methods
nlw0 Jul 9, 2022
560c643
added compat string
nlw0 Jul 9, 2022
429a235
copying extended help from #43203
nlw0 Jul 24, 2022
492bf41
Merge branch 'master' into patch-1
nlw0 May 9, 2023
55fbc4f
Update base/iterators.jl
nlw0 May 9, 2023
37f4736
Update base/iterators.jl
nlw0 May 28, 2023
00bd697
minor
May 28, 2023
a7fbd09
Update test/iterators.jl
nlw0 May 28, 2023
ef4743a
missing end
May 28, 2023
0d1518c
Merge branch 'master' into patch-1
nlw0 May 28, 2023
b20b110
unfold eltype as a keyword argument
May 29, 2023
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
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ New library features
* A `CartesianIndex` is now treated as a "scalar" for broadcasting ([#47044]).
* `printstyled` now supports italic output ([#45164]).
* `parent` and `parentindices` support `SubString`s
* `Iterators.unfold` creates an iterator from a transition function and an initial state
([#44873], [#43203]).

Standard library changes
------------------------
Expand Down
95 changes: 94 additions & 1 deletion base/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import .Base:
getindex, setindex!, get, iterate,
popfirst!, isdone, peek, intersect

export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, flatmap
export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, flatmap, unfold

if Base !== Core.Compiler
export partition
Expand Down Expand Up @@ -1543,4 +1543,97 @@ only(x::NamedTuple) = throw(
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element")
)

"""
unfold(f, initialstate, [eltype])

Iterable object that generates values from an initial state and a transition
function `f(state)`. The function must follow the same rules as `iterate`.
It returns either `(newvalue, newstate)` or `nothing`, in which case the
sequence ends.

The optional parameter `eltype` can specify the type of the generated values.

See also: [`iterate`](@ref), [the iteration interface](@ref man-interface-iteration)

!!! compat "Julia 1.9"
This function was added in Julia 1.9.

# Examples

```jldoctest
julia> fib = Iterators.unfold((1,1)) do (a,b)
a, (b, a+b)
end;

julia> reduce(hcat, Iterators.take(fib, 7))
1×7 Matrix{Int64}:
1 1 2 3 5 8 13

julia> frac(c, z=0.0im) = Iterators.unfold((c, z), eltype=ComplexF64) do (c, z)
if real(z * z') < 4
z, (c, z^2 + c)
else
nothing
end
end;

julia> [count(Returns(true), frac(-0.835-0.2321im, (k+j*im)/6)) for j in -4:4, k in -8:8]
9×17 Matrix{Int64}:
2 2 2 3 3 3 5 41 8 4 3 3 2 2 2 2 1
2 3 5 4 5 8 20 11 17 23 4 3 3 3 2 2 2
4 10 17 12 7 56 18 58 33 22 6 5 4 5 4 3 2
26 56 15 13 18 23 13 14 27 46 8 9 16 12 8 4 3
10 7 62 17 16 23 11 12 39 12 11 23 16 17 62 7 10
3 4 8 12 16 9 8 46 27 14 13 23 18 13 15 56 26
2 3 4 5 4 5 6 22 33 58 18 56 7 12 17 10 4
2 2 2 3 3 3 4 23 17 11 20 8 5 4 5 3 2
1 2 2 2 2 3 3 4 8 41 5 3 3 3 2 2 2
```

# Extended help

The interface for `f` is very similar to the interface required by `iterate`, but `unfold` is simpler to use because it does not require you to define a type. You can use this to your advantage when prototyping or writing one-off iterators.

You may want to define an iterator type instead either for readability, for defining `IteratorSize`, or to dispatch on the type of your iterator.

`unfold` is related to a `while` loop because:
```julia
collect(unfold(f, initialstate))
```
is roughly the same as:
```julia
acc = []
state = initialstate
while true
x = f(state)
isnothing(x) && break
element, state = x
push!(acc, element)
end
```
But the `unfold` version may produce a more strictly typed vector and can be easily modified to return a lazy collection by removing `collect()`.

In Haskell and some other functional programming environments, this function is known as `unfoldr`.
"""
function unfold(f, initialstate; eltype::Type{Eltype}) where {Eltype}
Iterators.rest(Unfold{Eltype}(f), initialstate)
end
function unfold(f, initialstate)
Iterators.rest(Unfold{nothing}(f), initialstate)
end
struct Unfold{Eltype, FuncType}
f::FuncType

Unfold{Eltype}(f) where {Eltype} = new{Eltype, typeof(f)}(f)
end
Unfold(f) = Unfold{nothing}(f)

Base.eltype(::Type{Unfold{Eltype, F}}) where {Eltype, F} = Eltype
Base.eltype(::Type{<:Unfold{nothing}}) = Any
Base.IteratorEltype(::Type{<:Unfold{nothing}}) = EltypeUnknown()
Base.IteratorEltype(::Type{<:Unfold}) = HasEltype()
Base.IteratorSize(::Type{<:Unfold}) = SizeUnknown()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is SizeUnknown() not the default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not really familiar with how this works, but there are many similar definitions in this file, including for Filter and TakeWhile.

Copy link
Contributor

Choose a reason for hiding this comment

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

No, the default is HasLength(). This is ok.


Base.iterate(it::Unfold, state) = it.f(state)

end
1 change: 1 addition & 0 deletions doc/src/base/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Base.Iterators.accumulate
Base.Iterators.reverse
Base.Iterators.only
Base.Iterators.peel
Base.Iterators.unfold
```
39 changes: 39 additions & 0 deletions test/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1001,3 +1001,42 @@ end
end
@test v == ()
end

@testset "Unfold" begin
@test isempty(Iterators.unfold(identity, nothing))

unfold61 = Iterators.unfold(1) do x
if x < 2^26
return x, x+x
else
return nothing
end
end
@test all(enumerate(unfold61)) do (i,x)
2^(i-1) == x
end

aa, bb = eachcol(randn(11,2))
vals = map(aa, bb) do a, b iterate(Iterators.unfold(a) do x x, b end) end
@test all(zip(aa, bb) .== vals)

struct init_sentinel end
@testset "Unfold replicates `iterate` calls" for myitr in [1:2:4, (-5:5)*π, (), (1,2), randn(5)]
myUnfold = Iterators.unfold(init_sentinel()) do state
if state isa init_sentinel
return iterate(myitr)
else
return iterate(myitr, state)
end
end
@test Iterators.map(==, myUnfold, myitr) |> all
end

fibs = Iterators.unfold(Int64.((1,1))) do (a,b) a, (b, a+b) end
fibO1(n) = (MathConstants.φ^n - (1-MathConstants.φ)^n) / √5
@test 93 == Iterators.take(Iterators.map(
!isapprox,
fibs,
(fibO1(x) for x in Iterators.countfrom(1))
), 111) |> collect |> findfirst
end