Подтвердить что ты не робот

Трубы 3.0: нелинейные топологии

Я смотрю на пакет пакетов 3.0 для обработки потока. Учебник очень хорошо сделан и очень ясен, за исключением того, что я не могу обернуть голову вокруг раздела "zip and merge".

Моя цель - объединить трубы, похожие на ArrowChoice:

  • У меня есть уникальный производитель Либо a
  • Я хотел бы применить первый канал к значениям Left, а другой - к значениям Right
  • Затем мне хотелось бы объединить результаты и продолжить работу с трубопроводом


+----------+                   +------+ - filterLeft ->  pipe1 -> +------------+ 
| producer | - (Either a a) -> | fork |                           | mergeD (?) |
+----------+                   +------+ - filterRight -> pipe2 -> +------------+

Я определяю fork как в учебнике:

fork () = 
    runIdentityP . hoist (runIdentityP . hoist runIdentityP) $ forever $ do
        a <- request ()
        lift $ respond a
        lift $ lift $ respond a

oddOrEven x = if odd x then Left x else Right x
producer = fromListS [1..0] >-> mapD oddOrEven
isLeft (Left _) = True
isLeft (Right _) = False
isRight = not . isLeft
filterLeft = filterD isLeft
filterRight = filterD isRight
pipe1 = mapD (\x -> ("seen on left", x))
pipe2 = mapD (\x -> ("seen on right", x))

p1 = producer >-> fork    

Проблема в том, что я не могу сделать типы правильными. В учебнике показано только, как запустить внутреннюю (поднятую) цепочку труб как самостоятельную сессию, но я хотел бы иметь возможность повторно вводить ее значения в канал, а не просто влиять на них. Я, конечно, старался следовать этим типам, но они очень быстро причесались.

Может ли кто-нибудь помочь мне в этом? Спасибо заранее.

(PS: пример такого типа топологии был бы хорошим дополнением к учебнику, или даже лучше раздел о том, как эмулировать материал Control.Arrow, используя каналы)

4b9b3361

Ответ 1

Абстракция pipe не поддерживает алмазные топологии или любую форму поведения Arrow. Это не проблема API, но нет правильного или четко определенного поведения для такого сценария.

Чтобы объяснить, почему, позвольте мне упростить вашу диаграмму до следующего:

          +----+
          | pL |
+----+ => +----+ => +----+
| p1 |              | p2 |
+----+ => +----+ => +----+
          | pR |
          +----+

Представьте, что мы находимся в трубе p1, а мы respond - pL. Если вы помните учебник, законы прокси требуют, чтобы каждый блок respond блокировался до восходящего потока. Это означает, что p1 не может восстановить контроль до pL request. Итак, в этот момент мы имеем:

  • p1 заблокировано, ожидая request от pL

Однако предположим, что pL еще не request и вместо этого respond со своим значением до p2. Итак, теперь мы имеем:

  • p1 заблокировано, ожидая request от pL
  • pL заблокировано, ожидая request из p2

Теперь предположим, что p2 вместо request из pR. Законы прокси говорят, что p2 не может восстановить контроль до pR respond снова. Теперь мы имеем:

  • p1 заблокировано, ожидая request от pL
  • pL заблокировано, ожидая request из p2
  • p2 заблокировано в ожидании respond от pR

Теперь, что происходит, когда pR request значение из p1? Если мы проконсультируем наш список блоков, p1 по-прежнему блокируется в ожидании request от pL, поэтому он не имеет формы для получения request из pR. Нет правильного способа "связать узел", так сказать, даже если pL и pR имеют общую подпись request.

В более общем плане законы прокси обеспечивают следующие два инварианта:

  • Каждая труба "вверх по течению" активной трубы будет заблокирована на respond
  • Каждая труба "вниз по течению" из активной трубы будет заблокирована на request

Циклы или алмазы нарушают эти инварианты. Вот почему в учебнике очень кратко отмечается, что циклические топологии не "имеют смысл".

Вы можете понять, почему бриллианты нарушают этот инвариант в примере, который я вам только что дал. Когда p1 имел управление, он находился выше pR, что означало бы, что pR был заблокирован на request. Однако, когда p2 получил контроль, он находился ниже pR, что означало бы, что pR был заблокирован на respond. Это приводит к противоречию, поскольку pR еще не изменилось, поскольку управление проходило через pL, а не pR, чтобы перейти к p2.

Машины

Итак, у вас есть два решения вашей проблемы. одно решение состоит в том, чтобы просто вставить желаемое поведение расщепления в один канал. Вы определяете канал pE, который объединяет поведение pL и pR в один канал.

Более элегантное решение этой проблемы - это что-то в стиле Эдварда machines. Вы определяете более ограниченную абстракцию, которая менее мощна, чем прокси-серверы, поддерживающие ArrowChoice, вы делаете свой материал стрелки в пределах этой абстракции, а затем, когда вы закончите, вы обновите его до прокси.

Если вы прищурились, вы могли бы притвориться, что в Haskell есть категория доступных в настоящее время абстракций coroutine, которые являются частичным порядком. Абстракции Coroutines - это объекты, а стрелка из абстракции сопрограммы C1 для абстракции сопрограммы C2 означает, что вы можете вставлять сопрограммы типа C1 в сопрограммы типа C2 (т.е. C1 является ненадлежащим подмножеством C2).

В этом частичном порядке прокси, вероятно, будут терминальным объектом, что означает, что вы можете думать о прокси-серверах как языке ассемблера сопрограмм. Следуя аналогии с языком ассемблера, прокси предоставляют меньше гарантий, но вы можете вставлять более ограничительные абстракции сопрограммы (то есть языки более высокого уровня) в пределах прокси. Эти языки более высокого уровня предоставляют большие ограничения, которые позволяют создавать более мощные абстракции (т.е. Экземпляр Arrow).

Если вам нужен тривиальный пример этого, рассмотрим одну из простейших абстракций сопрограммы: стрелка Клейсли:

newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }

instance Category (Kleisli m) where
    id = Kleisli return
    (Kleisli f) . (Kleisli g) = Kleisli (f <=< g)

Стрелки Kleisli определенно более ограничительны, чем прокси, но из-за этого ограничения они поддерживают экземпляр Arrow. Поэтому всякий раз, когда вам нужен экземпляр Arrow, вы пишете свой код с помощью стрелок Kleisli и объединяете его с помощью нотации Arrow, а затем, когда вы закончите, вы можете "скомпилировать" этот код более высокого уровня Kleisli с кодом сборки прокси, используя mapMD:

kleisliToProxy :: (Proxy p) => Kleisli m a b -> () -> Pipe p a b m r
kleisliToProxy (Kleisli f) = mapMD f

Эта компиляция подчиняется законам функтора:

kleisliToProxy id = idT

kleisliToProxy (f . g) = kleisliToProxy f <-< kleisliToProxy g

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