2016-12-10 6 views
4

Очень удобно использовать Tasks , чтобы выразить ленивую коллекцию/генератор.Лучше, чем использовать `Задача/производить/потреблять` для ленивых коллекций, выражать как сопрограммы

Например:

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 - не самое страшное в мире. В конце концов, часто бывает трудно узнать, когда вы закончите, не пытаясь найти следующее состояние. Можно было бы надеяться, что done всегда будет вызываться до next. Тем не менее это не здорово, так как происходит следующее:

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 s делают бедные итераторы. Во многих случаях они не совместимы с другим кодом, который ожидает итератор. (Во многих они есть, но трудно сказать, из чего). Это потому, что Task s не предназначены для использования в качестве итераторов в этих «генераторных» функциях.Они предназначены для низкоуровневого потока управления. И оптимизированы как таковые.

Итак, что является лучшим способом? Написание итератор для fib не так уж плохо:

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

Но это немного менее интуитивным. Для более сложных функций это гораздо менее приятно.

Так что мой вопрос: Что может быть лучшим способом иметь что-то, что работает как в качестве задачи, как способ создания итератора из одной функции, но это хорошо себя ведет?

Я не удивлюсь, если кто-то уже написал пакет с макросом, чтобы решить эту проблему.

ответ

1

Как о следующем (использует fib определенный в 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 (сейчас на дискурсе платформе). В любом случае, используя определенную 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).


Тем не менее, есть определенные предостережения:

  • Caveat 1: Задача состоит в том REQUIRED иметь возвращаемое значение, означающий конечный элемент в итерации, в противном случае " может произойти непредвиденное "поведение.

    Я предполагаю, что разработчики выбрали первый подход, чтобы избежать такого рода «непреднамеренного» вывода; однако I считают, что это должно было быть ожидаемым поведением! Ожидается, что задача, которую предполагается использовать в качестве итератора, должна определить по соответствующей конечной точке итерации (с помощью чистого возвращаемого значения) по дизайну!

    Пример 1: неправильный способ сделать это

    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); 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; 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. 
    
  • Caveat 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 
    
  • Caveat 3: С этой схеме ожидается поведение, которое вы можете 'продолжать, где вы остановились. например

    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 е! :)


Сказав все это, есть философская точка о том, даже имеет значения то, что путь Целевой ведет себя с start, next и done командой вопросов на всех, в том, что они функции считаются «an informal interface»: то есть они должны быть «под капотом», а не предназначены для вызова вручную.

Таким образом, до тех пор, пока они выполняют свою работу и возвращают ожидаемые значения итераций, вы не должны слишком беспокоиться о том, как они делают это под капотом, даже если технически они не совсем следуют «spec при этом, так как вы никогда не должны были называть их вручную в первую очередь.