Ответ 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 до абстракции прокси, чтобы их смешивать.