Какова реальная польза от параметра типа восходящего потока?
Я пытаюсь понять различия между различными реализациями концепции труб. Одна из отличий между трубопроводом и трубами заключается в том, как они соединяют трубы вместе. Канал имеет
(>+>) :: 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
обращается к концам ввода-вывода в виде подмножества синтаксического анализа.