比使用 `Task/produce/consume` 更好的方法将惰性集合表达为协程 [英] Better way than using `Task/produce/consume` for lazy collections express as coroutines

查看:21
本文介绍了比使用 `Task/produce/consume` 更好的方法将惰性集合表达为协程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用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.

值得商榷的是,该状态应该是 immutablenext 返回一个新状态,因此可以简单地进行 tee 编辑.(另一方面,分配新内存——虽然在堆栈上)

Debate-ably, that state should be immutable with next returning a new state, so that can be trivially teeed. (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 Tasks 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 Tasks 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆