2016-12-10 8 views
4

Tasks を使用すると、遅延コレクション/ジェネレータを表現するのが非常に便利です。レイジーコレクションの `Task/produce/consume`をコルーチンとして表現するよりも良い方法

例:

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 

しかし、彼らはすべての良いイテレータの規則に従いません。 彼らは

彼らは彼らではなく、イテレータオブジェクト自体を変異されて返された状態state

start(fib()) == nothing #It has no state 

を使用しないことができるように彼らがひどく振る舞っています。 適切なイテレータは、それ自体を変更するのではなく、その状態を使用するため、複数の呼び出し元が一度に反復することができます。 startでその状態を作成し、nextの間にそれを前進させます。

この状態はimmutableとなります。nextは新しい状態を返すため、普通はteeとなります。 (一方で、新しいメモリの割り当て - スタック上でも)

さらに、隠された状態では、nextの間に進んでいません。 以下は動作しません:

@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中に進んでいる: 次のような作品:

@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の状態は、世界で最悪のことではありません。 結局のところ、次の状態を見つけようとすることなく、完了した時点を知ることが難しい場合がよくあります。 1つは、の前に常にdoneが呼び出されると思います。次の問題が発生したため、 はまだそれは、素晴らしいではありません。

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は複数回も安全に呼び出すことができます。


基本的にはTaskです。多くの場合、イテレータを必要とする他のコードと互換性がありません。 (多くの人はそうですが、どの人からそれを知るのは難しいです)。 Taskは実際にはこれらの「ジェネレーター」関数のイテレーターとして使用されていないためです。低レベルの制御フローを意図しています。 そのように最適化されています。

だから、もっと良い方法は何ですか?

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() 

しかし少し小さい直感的である: fibのためのイテレータを書くことは悪くないです。 もっと複雑な関数の場合は、それほど優れていません。

私の質問は次のとおりです。 単一の関数からイテレータを構築する方法として、タスクがそうであるように機能するものはありますが、うまく動作しますか?

これを解決するために誰かがすでにマクロを使ってパッケージを書いていても、私は驚くことはありません。

答えて

1

は、どのように(OPで定義されてfibを使用しています)以下について:

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 

これは良い質問です、そして(今談話プラットフォーム上の)可能性の方が適しジュリアリストにあります。いずれにしても、定義されたNewTaskを使用すると、最近のStackOverflow質問に対する改善された答えが可能です。参照:タスクのhttps://stackoverflow.com/a/41068765/3580870

1

現在のイテレータインターフェイスは非常に簡単です:

# in share/julia/base/task.jl 
275 start(t::Task) = nothing 
276 function done(t::Task, val) 
277  t.result = consume(t) 
278  istaskdone(t) 
279 end 
280 next(t::Task, val) = (t.result, nothing) 

開発者がdone機能ではなく、next機能における消費段階を置くことを選んだ理由はわかりません。あなたの奇妙な副作用を引き起こしているのはこれです。これは私があなたの質問への答えとして提案するものです。したがって、

import Base.start; function Base.start(t::Task) return t end 
import Base.next; function Base.next(t::Task, s::Task) return consume(s), s end 
import Base.done; function Base.done(t::Task, s::Task) istaskdone(s) end 

:私には、このようなインターフェイスを実装するためにはるかに簡単に聞こえます。

私は、この単純な実装ははるかに意味があり、上記の基準を満たし、意味のある状態を出力するという望ましい結果を持っていると思います。 (本当にしたいのであれば消費を伴わない限り、「検査する」ことができます:p)


しかしながら、特定の注意事項があります

  • 警告1:タスクは、「そうでなければ、反復の最後の要素を意味する、戻り値を持つように必要あります予期しない動作が発生する可能性があります。

    私はdevsがこのような「意図しない」出力を避けるための最初のアプローチを選択したと仮定しています。しかし、私はこれは実際に期待された行動であったはずだと信じています!イテレータとして使用されることが予想されるタスクは、適切な反復エンドポイントを(明確な戻り値によって)設計によって定義することが期待されます。

    例1:(中を:それを

    julia> t = Task() do; produce(1); produce(2); produce(3); produce(4); end; 
    julia> collect(t) |> show 
    Any[1,2,3,4,()] # last item is the return value of the produce statement, 
           # which returns any items passed to it by the last 
           # 'consume' call; in this case an empty tuple. 
           # Presumably not the intended output! 
    

    例3を行う別の間違った方法をそれを

    julia> t = Task() do; for i in 1:10; produce(i); end; end; 
    julia> collect(t) |> show 
    Any[1,2,3,4,5,6,7,8,9,10,nothing] # last item is a return value of nothing 
                # correponding to the "return value" of the 
                # for loop statement, which is 'nothing'. 
                # Presumably not the intended output! 
    

    例2を行うには間違った方法私の謙虚な意見)それは正しい方法です!

    julia> t = Task() do; produce(1); produce(2); produce(3); return 4; end; 
    julia> collect(t) |> show 
    [1,2,3,4] # An appropriate return value ending the Task function ensures an 
          # appropriate final value for the iteration, as intended. 
    
  • 2警告:タスクが変更されるべきではない /反復内側さらに消費これは意図的に反復で「スキップ」させるという理解を除いて、(一般にイテレータと共通要件) (これは最高のハックであり、おそらく推奨できません)。

    例:

    julia> t = Task() do; produce(1); produce(2); produce(3); return 4; end; 
    julia> for i in t; show(consume(t)); end 
    24 
    

    より微妙な例:

    julia> t = Task() do; produce(1); produce(2); produce(3); return 4; end; 
    julia> for i in t # collecting i is a consumption event 
         for j in t # collecting j is *also* a consumption event 
          show(j) 
         end 
         end # at the end of this loop, i = 1, and j = 4 
    234 
    
  • 警告3:それはあなたは 'あなたがオフに左続ける' ことができ振る舞いを期待されているこの方式では。例えば1は、タスクの前の消費状態から常にスタートにイテレータを好む場合

    julia> t = Task() do; produce(1); produce(2); produce(3); return 4; end; 
    julia> take(t, 2) |> collect |> show 
    [1,2] 
    julia> take(t, 2) |> collect |> show 
    [3,4] 
    

    しかし、スタート機能は、これを達成するように変更することができます

    興味深いことに
    import Base.start; function Base.start(t::Task) return Task(t.code) end; 
    import Base.next; function Base.next(t::Task, s::Task) consume(s), s end; 
    import Base.done; function Base.done(t::Task, s::Task) istaskdone(s) end; 
    
    julia> for i in t 
         for j in t 
          show(j) 
         end 
         end # at the end of this loop, i = 4, and j = 4 independently 
    1234123412341234 
    

    、ノートこの変形は、「警告2」から「内部消費」のシナリオにどのような影響を与えるか:

    julia> t = Task() do; produce(1); produce(2); produce(3); return 4; end; 
    julia> for i in t; show(consume(t)); end 
    1234 
    julia> for i in t; show(consume(t)); end 
    4444  
    

    を参照してくださいあなたが見つけることができれば、これはSENSを作る理由e! :)


はそれも仕方がタスクが、その中で、すべてのstartnext、およびdoneコマンドの問題で動作することを重要かどうかについての哲学的な点があり、このすべてを言って、これらのファンクションは "an informal interface"と見なされます。つまり、これらは "フードの下"の機能であり、手動で呼び出されることはありません。

が技術的にであっても、彼らが仕事をして期待される反復値を返す限り、彼らはフードの下でそれをどうするかについてあまり気にしてはいけません。あなたが最初に手動で呼び出すことは決してなかったからです。

関連する問題