比使用 `Task/produce/consume` 更好的方法将惰性集合表达为协程 [英] Better way than using `Task/produce/consume` for lazy collections express as coroutines
问题描述
使用Tasks
非常方便表达一个惰性集合/一个生成器.
It is very convenient to use Tasks
to express a lazy collection / a generator.
例如:
function fib()
Task() do
prev_prev = 0
prev = 1
produce(prev)
while true
cur = prev_prev + prev
produce(cur)
prev_prev = prev
prev = cur
end
end
end
collect(take(fib(), 10))
输出:
10-element Array{Int64,1}:
1
1
2
3
5
8
13
21
34
但是,它们根本不遵循良好的迭代器约定.他们的行为举止尽善尽美
However, they do not follow good iterator conventions at all. They are as badly behaved as they can be
他们不使用返回的状态state
start(fib()) == nothing #It has no state
所以他们改为改变迭代器对象本身.一个合适的迭代器使用它的状态,而不是改变自己,所以他们多个调用者可以一次迭代它.使用 start
创建该状态,并在 next
期间推进它.
So they are instead mutating the iterator object itself.
An proper iterator uses its state, rather than ever mutating itself, so they multiple callers can iterate it at once.
Creating that state with start
, and advancing it during next
.
值得商榷的是,该状态应该是 immutable
且 next
返回一个新状态,因此可以简单地进行 tee
编辑.(另一方面,分配新内存——虽然在堆栈上)
Debate-ably, that state should be immutable
with next
returning a new state, so that can be trivially tee
ed. (On the other hand, allocating new memory -- though on the stack)
此外,隐藏状态,在 next
期间没有前进.以下方法不起作用:
Further-more, the hidden state, it not advanced during next
.
The following does not work:
@show ff = fib()
@show state = start(ff)
@show next(ff, state)
输出:
ff = fib() = Task (runnable) @0x00007fa544c12230
state = start(ff) = nothing
next(ff,state) = (nothing,nothing)
相反,在 done
期间隐藏状态是高级的:以下作品:
Instead the hidden state is advanced during done
:
The following works:
@show ff = fib()
@show state = start(ff)
@show done(ff,state)
@show next(ff, state)
输出:
ff = fib() = Task (runnable) @0x00007fa544c12230
state = start(ff) = nothing
done(ff,state) = false
next(ff,state) = (1,nothing)
done
期间的推进状态并不是世界上最糟糕的事情.毕竟,通常情况下很难知道何时完成,而不去尝试寻找下一个状态.人们希望 done
总是在 next
之前被调用.仍然不是很好,因为发生了以下情况:
Advancing state during done
isn't the worst thing in the world.
After all, it is often the case that it is hard to know when you are done, without going to try and find the next state. One would hope done
would always be called before next
.
Still it is not great, since the following happens:
ff = fib()
state = start(ff)
done(ff,state)
done(ff,state)
done(ff,state)
done(ff,state)
done(ff,state)
done(ff,state)
@show next(ff, state)
输出:
next(ff,state) = (8,nothing)
这正是您现在所期望的.可以合理地假设 done
多次调用是安全的.
Which is really now what you expect. It is reasonably to assume that done
is safe to call multiple times.
基本上,Task
是糟糕的迭代器.在许多情况下,它们与需要迭代器的其他代码不兼容.(在许多情况下,它们是,但很难区分哪个来自哪个).这是因为在这些生成器"函数中,Task
并不是真正用作迭代器.它们用于低级控制流.并因此进行了优化.
Basically Task
s make poor iterators. In many cases they are not compatible with other code that expects an iterator. (In many they are, but it is hard to tell which from which).
This is because Task
s are not really for use as iterators, in these "generator" functions. They are intended for low-level control flow.
And are optimized as such.
那么更好的方法是什么?为 fib
写一个迭代器还不错:
So what is the better way?
Writing an iterator for fib
isn't too bad:
immutable Fib end
immutable FibState
prev::Int
prevprev::Int
end
Base.start(::Fib) = FibState(0,1)
Base.done(::Fib, ::FibState) = false
function Base.next(::Fib, s::FibState)
cur = s.prev + s.prevprev
ns = FibState(cur, s.prev)
cur, ns
end
Base.iteratoreltype(::Type{Fib}) = Base.HasEltype()
Base.eltype(::Type{Fib}) = Int
Base.iteratorsize(::Type{Fib}) = Base.IsInfinite()
但 is 有点不直观.对于更复杂的功能,它就不那么好了.
But is is a bit less intuitive. For more complex functions, it is much less nice.
所以我的问题是:有什么更好的方法可以像 Task 那样工作,作为从单个函数构建迭代器的一种方式,但行为良好?
如果有人已经编写了一个带有宏的包来解决这个问题,我不会感到惊讶.
I would not be surprised if someone has already written a package with a macro to solve this.
推荐答案
如下(使用OP中定义的fib
):
How about the following (uses fib
defined in OP):
type NewTask
t::Task
end
import Base: start,done,next,iteratorsize,iteratoreltype
start(t::NewTask) = istaskdone(t.t)?nothing:consume(t.t)
next(t::NewTask,state) = (state==nothing || istaskdone(t.t)) ?
(state,nothing) : (state,consume(t.t))
done(t::NewTask,state) = state==nothing
iteratorsize(::Type{NewTask}) = Base.SizeUnknown()
iteratoreltype(::Type{NewTask}) = Base.EltypeUnknown()
function fib()
Task() do
prev_prev = 0
prev = 1
produce(prev)
while true
cur = prev_prev + prev
produce(cur)
prev_prev = prev
prev = cur
end
end
end
nt = NewTask(fib())
take(nt,10)|>collect
这是一个很好的问题,可能更适合 Julia 列表(现在在 Discourse 平台上).在任何情况下,使用定义的 NewTask 来改进对最近 StackOverflow 问题的回答是可能的.请参阅:https://stackoverflow.com/a/41068765/3580870
This is a good question, and is possibly better suited to the Julia list (now on Discourse platform). In any case, using defined NewTask an improved answer to a recent StackOverflow question is possible. See: https://stackoverflow.com/a/41068765/3580870
这篇关于比使用 `Task/produce/consume` 更好的方法将惰性集合表达为协程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!