… so I thought I’d post it here and see what people thought.
Lazy sequences have been a point of frustration for me with Julia. Python is my main background, and I always liked generator functions, and I really enjoy playing with lazy types in various functional languages.
In Julia, channels can be used in a way similar to generator functions, but the performance is bad. Thunk-based solutions like Lazy.jl are super cool, but performance there is also problematic. Making an efficient lazy iterator in Julia requires at least defining a type and an iterate method for it—as well as Base.eltype
and Base.IteratorSize
for most practical cases. This seems boiler-plate-y. Required infinite Fibonacci example:
struct Fibs end
Base.iterate(f::Fibs) = iterate(f, (0, 1))
Base.iterate(::Fibs, (a, b)) = a, (b, a+b)
Base.IteratorSize(::Fibs) = Base.IsInfinite()
Base.eltype(::Fibs) = Int
foreach(println, Fibs())
This is actually fast!
My thought was to make a type that generalized this. This is what I came up with:
struct Sequence{F <: Function, I}
f::F
init::I
end
Base.iterate(s::Sequence) = iterate(s, s.init)
Base.iterate(s::Sequence, state) = s.f(state)
Base.IteratorSize(::Sequence) = Base.SizeUnknown()
Now, your infinite fibonacci sequence looks like this:
fibs = Sequence((0, 1)) do (a, b)
a, (b, a+b)
end
And it is just as fast as the other method.
If you want a sequence that terminates, it looks like this:
counter(stop) = Sequence(1) do n
n > stop ? nothing : (n, n+1)
end
This works well, but it’s still tricky to “collect” because it lacks an implementation for eltype
. You can either give a type explicitly to the collect function, or you can use something like this:
_get_types(s::Sequence) =
Base.return_types(s.f, (typeof(s.state),))[1] |> _get_types
_get_types(::Type{Union{Nothing, S}}) where S = _get_types(S)
_get_types(::Type{Tuple{El,State}}) where {El,State} = (el=El, state=State)
Base.eltype(s::Sequence) = _get_types(s).el
Note that this is an imperfect heuristic. It can fail if the type of the state can change in such a way that will change the return types. This seems to be pretty rare in practice.
Is anyone else interested in this kind of thing? Would it be worth packaging it up and distributing it?
Edit: @tkf explained in a later post a better way to implement eltype:
Base.IteratorEltype(::Sequence) = Base.EltypeUnknown()