Какова реальная польза от параметра типа восходящего потока?

Я пытаюсь понять различия между различными реализациями концепции труб. Одна из отличий между трубопроводом и трубами заключается в том, как они соединяют трубы вместе. Канал имеет

(>+>) :: Monad m
      => Pipe l a b r0 m r1 -> Pipe Void b c r1 m r2 -> Pipe l a c r0 m r2

а трубы имеют

(>->) :: (Monad m, Proxy p)
      => (b' -> p a' a b' b m r) -> (c' -> p b' b c' c m r) -> c' -> p a' a c' c m r

Если я правильно ее понимаю, с трубами, когда какой-либо труба из двух остановок, его результат возвращается, а другой - остановлен. С кабелепроводом, если левая труба закончена, ее результат направляется вниз по направлению к правой трубе.

Интересно, в чем преимущество подхода кабелепровода? Я хотел бы увидеть некоторый пример (желательно в реальном мире), который легко реализовать с использованием кабелепровода и >+>, но hard (er) для реализации с использованием труб и >->.

Ответ 1

По моему опыту, реальные преимущества терминаторов верхнего уровня очень тонкие, поэтому на данный момент они скрыты от публичного API. Я думаю, что я использовал их только в одном фрагменте кода (wai-extra multipart parsing).

В своей наиболее общей форме труба позволяет вам создавать как поток выходных значений, так и конечный результат. Когда вы соединяете эту трубку с другой нисходящей трубой, тогда поток выходных значений становится потоком входного потока ниже по потоку, а конечный результат восходящего потока становится ниже по потоку "терминатором вверх по течению". Поэтому с этой точки зрения наличие произвольных терминаторов восходящего потока допускает симметричный API.

Однако на практике очень редко используется такая функциональность, и поскольку она просто путает API, она была скрыта в модуле .Internal с выпуском 1.0. Один теоретический вариант использования может быть следующим:

  • У вас есть источник, который создает поток байтов.
  • A Conduit, который потребляет поток байтов, вычисляет хеш в качестве конечного результата и передает все байты ниже по течению.
  • Раковина, которая потребляет поток байтов, например, для хранения их в файле.

С терминаторами верхнего уровня вы можете подключить эти три устройства и получить результат, полученный от Conduit, как конечный результат конвейера. Однако в большинстве случаев есть альтернативное, более простое средство для достижения тех же целей. В этом случае вы можете:

  • Используйте conduitFile для хранения байтов в файле и превратите хеш-канал в хэш-приемник и поместите его вниз по течению
  • Используйте zipSinks, чтобы объединить как хэш-приемник, так и приемник для записи файлов в один приемник.

Ответ 2

Классическим примером чего-то более легкого в использовании с conduit в настоящее время является обработка конца ввода из восходящего потока. Например, если вы хотите сбросить список значений и связать результат в конвейере, вы не сможете сделать это в pipes без разработки дополнительного протокола поверх pipes.

Фактически, это именно то, что решает будущая библиотека pipes-parse. Он разрабатывает протокол Maybe поверх pipes, а затем определяет удобные функции для ввода входных данных с восходящего потока, которые уважают этот протокол.

Например, у вас есть функция onlyK, которая берет трубку и обертывает все выходы в Just, а затем заканчивается символом Nothing:

onlyK :: (Monad m, Proxy p) => (q -> p a' a b' b m r) -> (q -> p a' a b' (Maybe b) m r)

У вас также есть функция justK, которая определяет функтор из труб, которые Maybe -unaware для труб, которые Maybe -aware для обратной совместимости

justK :: (Monad m, ListT p) => (q -> p x a x b m r) -> (q -> p x (Maybe a) x (Maybe b) m r)

justK idT = idT
justK (p1 >-> p2) = justK p1 >-> justK p2

И затем, когда у вас есть Producer, который соблюдает этот протокол, вы можете использовать большое количество парсеров, которые абстрактны над тегом Nothing для вас. Самый простой - draw:

draw :: (Monad m, Proxy p) => Consumer (ParseP a p) (Maybe a) m a

Он извлекает значение типа a или терпит неудачу в прокси-трансляторе ParseP, если в восходящем потоке закончилось входное значение. Вы также можете взять сразу несколько значений:

drawN :: (Monad m, Proxy p) => Int -> Consumer (ParseP a p) (Maybe a) m [a]

drawN n = replicateM n draw  -- except the actual implementation is faster

... и несколько других приятных функций. Пользователь никогда не должен напрямую взаимодействовать с концом входного сигнала.

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