比使用“任务/生产/消费"来表现为协程的惰性集合更好的方法 [英] 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()
但是有点不那么直观. 对于更复杂的功能,它就不太好了.
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
这篇关于比使用“任务/生产/消费"来表现为协程的惰性集合更好的方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!