Какие трубы / трубопровод пытаются решить

Я видел, как люди рекомендовали библиотеку pipe/conduit для различных задач, связанных с ленивым вводом-выводом. Какую проблему решают эти библиотеки?

Кроме того, когда я пытаюсь использовать некоторые библиотеки, связанные с хакерством, весьма вероятно, что существуют три разные версии. Пример:

Это меня смущает. Для моих задач синтаксического анализа я должен использовать attoparsec или pipe-attoparsec/attoparsec-conduit? Какие преимущества дает мне версия с трубами/кондуитами по сравнению с обычным ванильным аттопарсеком?


person Sibi    schedule 30.03.2014    source источник


Ответы (3)


Ленивый ввод-вывод

Ленивый ввод-вывод работает так

readFile :: FilePath -> IO ByteString

где ByteString гарантированно читается только по частям. Для этого мы могли бы (почти) написать

-- given `readChunk` which reads a chunk beginning at n
readChunk :: FilePath -> Int -> IO (Int, ByteString)

readFile fp = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    chunks      <- readChunks n'
    return (chunk <> chunks)

но здесь мы отмечаем, что действие ввода-вывода readChunks n' выполняется до возврата даже частичного результата, доступного как chunk. Это значит, что мы совсем не ленивы. Для борьбы с этим мы используем unsafeInterleaveIO

readFile fp = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    chunks      <- unsafeInterleaveIO (readChunks n')
    return (chunk <> chunks)

что заставляет readChunks n' возвращаться немедленно, а IO действие выполняется только тогда, когда этот переход принудительно выполняется.

Это опасная часть: с помощью unsafeInterleaveIO мы отложили набор IO действий до недетерминированных моментов в будущем, которые зависят от того, как мы потребляем наши фрагменты ByteString.

Исправление проблемы с сопрограммами

Что мы хотели бы сделать, так это вставить шаг обработки фрагмента между вызовом readChunk и рекурсией на readChunks.

readFileCo :: Monoid a => FilePath -> (ByteString -> IO a) -> IO a
readFileCo fp action = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    a           <- action chunk
    as          <- readChunks n'
    return (a <> as)

Теперь у нас есть возможность выполнять произвольные IO действий после загрузки каждого маленького фрагмента. Это позволяет нам выполнять гораздо больше работы постепенно, без полной загрузки ByteString в память. К сожалению, это не очень композиционно — нам нужно построить наше потребление action и передать его нашему ByteString производителю, чтобы он заработал.

Ввод-вывод на основе каналов

По сути, это то, что решает pipes — он позволяет нам легко составлять эффективные сопрограммы. Например, теперь мы пишем нашу программу чтения файлов как Producer, которую можно рассматривать как "потоковую передачу" фрагментов файла, когда ее эффект наконец запускается.

produceFile :: FilePath -> Producer ByteString IO ()
produceFile fp = produce 0 where
  produce n = do
    (n', chunk) <- liftIO (readChunk fp n)
    yield chunk
    produce n'

Обратите внимание на сходство между этим кодом и кодом readFileCo выше — мы просто заменяем вызов действия сопрограммы на yielding chunk, который мы создали до сих пор. Этот вызов yield создает тип Producer вместо необработанного действия IO, которое мы можем скомпоновать с другими типами Pipes, чтобы построить хороший конвейер потребления, называемый Effect IO ().

Все это построение каналов выполняется статически без фактического вызова каких-либо действий IO. Вот как pipes упрощает написание сопрограмм. Все эффекты срабатывают одновременно, когда мы вызываем runEffect в нашем действии main IO.

runEffect :: Effect IO () -> IO ()

Аттопарсек

Итак, почему вы хотите подключить attoparsec к pipes? Ну, attoparsec оптимизирован для ленивого синтаксического анализа. Если вы производите фрагменты, подаваемые парсеру attoparsec, эффективным способом, то вы окажетесь в тупике. Ты мог бы

  1. Используйте строгий ввод-вывод и загрузите всю строку в память только для того, чтобы лениво использовать ее с помощью вашего синтаксического анализатора. Это просто, предсказуемо, но неэффективно.
  2. Используйте ленивый ввод-вывод и потеряйте возможность рассуждать о том, когда ваши производственные эффекты ввода-вывода будут фактически запущены, что приведет к возможным утечкам ресурсов или закрытым исключениям дескриптора в соответствии с графиком потребления ваших проанализированных элементов. Это более эффективно, чем (1), но может легко стать непредсказуемым; или,
  3. Используйте pipes (или conduit) для создания системы сопрограмм, в которую входит ваш ленивый парсер attoparsec, позволяющий ему обрабатывать как можно меньше входных данных, в то же время создавая максимально лениво анализируемые значения по всему потоку.
person J. Abrahamson    schedule 30.03.2014
comment
Спасибо, что такое оператор <> в функциях readChunks? Гугл не помог. - person Sibi; 30.03.2014
comment
Извините, что Monoid ленивой ByteString. Идея должна заключаться в том, что ленивые ByteString — это просто ленивые списки строгих ByteString, поэтому (<>) — это не что иное, как (++) в этих списках. - person J. Abrahamson; 30.03.2014
comment
@Sibi Вероятно, инфикс mappend. Хайу! можно найти: holumbus.fh-wedel.de/hayoo /hayoo.html?query=%3C%3E - person Cirdec; 30.03.2014
comment
Что касается пункта 1 в разделе attoparsec, что вы подразумеваете под строгим вводом-выводом? Относится ли это к исходной функции readFile без unsafeInterleaveIO, которая считывает весь файл в память? - person Sibi; 30.03.2014
comment
@ Сиби Да, точно. Чтобы узнать больше об этом, см. Иоахим Брайтнер, Как составить список в монаде. В частности, изучите проблему, которую он обрисовывает в общих чертах (и для которой не предлагает четкого решения), как именно ту, которую решают pipes и conduit. - person J. Abrahamson; 31.03.2014
comment
На самом деле это не ключ к остальным, но unsafeInterleaveIO должен быть дальше; ленивый ввод-вывод ленив с самого начала. - person dfeuer; 02.02.2015
comment
@dfeuer Я тоже понял эту ошибку, поэтому я рад, что вы увековечили ее. Я надеялся, что это было более понятно в слегка строгой форме, несмотря на некорректность. - person J. Abrahamson; 02.02.2015

Если вы хотите использовать аттопарсек, используйте аттопарсек

Для моих задач синтаксического анализа я должен использовать attoparsec или pipe-attoparsec/attoparsec-conduit?

И pipes-attoparsec, и attoparsec-conduit преобразуют данный attoparsec Parser в раковину/канал или трубу. Поэтому вы должны использовать attoparsec в любом случае.

Какие преимущества дает мне версия с трубами/кондуитами по сравнению с обычным ванильным аттопарсеком?

Они работают с трубами и кабелепроводом, чего не сделает ванильный (по крайней мере, не из коробки).

Если вы не используете канал или каналы и вас устраивает текущая производительность отложенного ввода-вывода, нет необходимости менять текущий поток, особенно если вы не пишете большое приложение и не обрабатываете большие файлы. Вы можете просто использовать attoparsec.

Однако это предполагает, что вы знаете недостатки отложенного ввода-вывода.

Что случилось с ленивым вводом-выводом? (проблемное исследование withFile)

Давайте не будем забывать ваш первый вопрос:

Какую проблему решают эти библиотеки?

Они решают проблему потоковой передачи данных (см. 1 и 3), который встречается в функциональных языках с отложенным вводом-выводом. Ленивый ввод-вывод иногда дает вам не то, что вы хотите (см. пример ниже), а иногда трудно определить фактические системные ресурсы, необходимые для конкретной ленивой операции (данные читаются/записываются в кусках/байтах/буферизуются/onclose/onopen…) .

Пример чрезмерной лени

import System.IO
main = withFile "myfile" ReadMode hGetContents
       >>= return . (take 5)
       >>= putStrLn

Это ничего не напечатает, так как оценка данных происходит в putStrLn, но в этот момент дескриптор уже закрыт.

Фиксация огня ядовитой кислотой

Хотя следующий фрагмент исправляет это, у него есть еще одна неприятная особенность:

main = withFile "myfile" ReadMode $ \handle -> 
           hGetContents handle
       >>= return . (take 5)
       >>= putStrLn

В этом случае hGetContents прочитает весь файл, чего вы изначально не ожидали. Если вы просто хотите проверить магические байты файла, который может иметь размер в несколько ГБ, это не вариант.

Правильное использование withFile

Решение, очевидно, заключается в take вещах в withFile контексте:

main = withFile "myfile" ReadMode $ \handle -> 
           fmap (take 5) (hGetContents handle)
       >>= putStrLn

Это кстати тоже решение упомянутое автором пайпов :

Это [..] отвечает на вопрос, который люди иногда задают мне о pipes, который я перефразирую здесь:

Если управление ресурсами не является основным направлением pipes, почему я должен использовать pipes вместо отложенного ввода-вывода?

Многие люди, которые задают этот вопрос, открыли для себя потоковое программирование благодаря Олегу, который сформулировал проблему отложенного ввода-вывода с точки зрения управления ресурсами. Однако я никогда не находил этот аргумент убедительным сам по себе; вы можете решить большинство проблем управления ресурсами, просто отделив получение ресурсов от ленивого ввода-вывода, например: [см. последний пример выше]

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

Вы можете просто использовать attoparsec [...][с отложенным вводом-выводом, предполагая], что вы знаете недостатки отложенного ввода-вывода.

использованная литература

person Zeta    schedule 30.03.2014
comment
Если вы думаете о ленивом вводе-выводе как о своего рода прозрачном параллелизме, вам будет намного проще думать о том, где могут возникнуть проблемы: если hGetContents разветвил поток, совершенно ясно, что немедленный возврат из withFile после этого может не дать потоку шанса. чтобы закончить чтение до того, как дескриптор файла будет закрыт. - person John Wiegley; 20.08.2015

Вот отличный подкаст с авторами обеих библиотек:

http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/

Это ответит на большинство ваших вопросов.


Короче говоря, обе эти библиотеки подходят к проблеме потоковой передачи, что очень важно при работе с вводом-выводом. По сути, они управляют передачей данных порциями, что позволяет вам, например. передать файл размером 1 ГБ, занимающий всего 64 КБ ОЗУ как на сервере, так и на клиенте. Без потоковой передачи вам пришлось бы выделять столько памяти на обоих концах.

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

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

person Nikita Volkov    schedule 30.03.2014
comment
Спасибо, это я уже видел. Но они не обсуждают вопросы (по крайней мере, в явном виде), которые я обозначил по вышеуказанному вопросу. - person Sibi; 30.03.2014