Что такое трубы/трубопроводы, пытающиеся решить
Я видел людей, рекомендующих библиотеку каналов/каналов для различных задач, связанных с отложенным вводом-выводом. Какую проблему эти библиотеки решают точно?
Кроме того, когда я пытаюсь использовать некоторые библиотеки, связанные с хакерскими атаками, весьма вероятно, что есть три разные версии. Пример:
Это смущает меня. Для моих задач разбора я должен использовать attoparsec или pipe-attoparsec/attoparsec -duit? Какую пользу дает версия для труб/каналов по сравнению с простой ванильной атопарсек?
Ответы
Ответ 1
Ленивый И.О.
Ленивый IO работает так
readFile :: FilePath -> IO ByteString
где 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
будет выполняться только при форсировании этого thunk.
В этом опасная часть: с помощью 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
для его запуска.
Трубы на основе IO
Это в основном то, что решает 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
выше - мы просто заменяем вызов действия сопрограммы на yield
chunk
мы создали до сих пор. Этот вызов yield
созданию типа Producer
вместо необработанного действия IO
которое мы можем составить с другими типами Pipe
для создания удобного конвейера потребления, называемого Effect IO()
.
Все это построение канала выполняется статически, без каких-либо действий IO
. Вот как pipes
позволяют вам писать свои сопрограммы более легко. Все эффекты запускаются сразу, когда мы вызываем runEffect
в нашем main
действии IO
.
runEffect :: Effect IO () -> IO ()
Attoparsec
Так почему вы хотите подключить attoparsec
к pipes
? Что ж, attoparsec
оптимизирован для ленивого разбора. Если вы производите куски, подаваемые к attoparsec
парсер в effectful образом, то вы будете в тупике. Вы могли бы
- Используйте строгий ввод-вывод и загружайте всю строку в память только для ленивого использования ее вашим анализатором. Это просто, предсказуемо, но неэффективно.
- Используйте ленивый ввод-вывод и потеряйте способность рассуждать о том, когда ваши производственные эффекты ввода-вывода будут фактически запущены, вызывая возможные утечки ресурсов или исключения закрытого дескриптора в соответствии с графиком потребления проанализированных элементов. Это более эффективно, чем (1), но может легко стать непредсказуемым; или же,
- Используйте
pipes
(или conduit
) для создания системы сопрограмм, которая включает в себя ваш ленивый анализатор attoparsec
позволяющий ему работать с минимально необходимым вводом, в то же время генерируя проанализированные значения как можно более лениво по всему потоку.
Ответ 2
Если вы хотите использовать attoparsec, используйте attoparsec
Для моих задач синтаксического анализа следует использовать attoparsec или pipe-attoparsec/attoparsec-conduit?
Оба pipes-attoparsec
и attoparsec-conduit
преобразуют данный attoparsec
Parser
в раковину/трубопровод или трубу. Поэтому вы должны использовать attoparsec
в любом случае.
Какая польза от версии для труб/кабелепроводов дает мне по сравнению с простой ванильной аттопарсекой?
Они работают с трубами и кабелепроводом, где ваниль не будет (по крайней мере, не из коробки).
Если вы не используете кабелепровод или трубки, и вы удовлетворены текущей производительностью вашего ленивого ввода-вывода, вам не нужно менять текущий поток, особенно если вы не пишете большое приложение или не обрабатываете большие файлы, Вы можете просто использовать attoparsec
.
Однако это предполагает, что вы знаете недостатки ленивого ввода-вывода.
Что с ленивым IO? (Исследование проблем withFile
)
Не забывайте свой первый вопрос:
Какую проблему решают эти библиотеки?
Они решают проблему потоковых данных (см. 1 и 3), которая встречается в функциональных языки с ленивым ИО. Lazy IO иногда дает вам не то, что вы хотите (см. Пример ниже), и иногда трудно определить фактические системные ресурсы, необходимые для конкретной ленивой операции (это чтение/запись данных в кусках/байтах/буферизация/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
[...] [с ленивым IO, предполагая], что вы знаете недостатки ленивого ввода-вывода.
Ссылки
Ответ 3
Здесь отличный подкаст с авторами обеих библиотек:
http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/
Он ответит на большинство ваших вопросов.
Короче говоря, обе эти библиотеки подходят к проблеме потоковой передачи, что очень важно при работе с IO. По сути, они управляют передачей данных в куски,
что позволяет вам, например, передайте 1 ГБ файл, используя только 64 КБ ОЗУ как на сервере, так и на клиенте. Без потоковой передачи вам пришлось бы выделять столько памяти на обоих концах.
Более старой альтернативой этим библиотекам является ленивый IO, но он заполнен проблемами и делает приложения подверженными ошибкам. Эти проблемы обсуждаются в подкасте.
Относительно того, какую из этих библиотек использовать, это скорее вопрос вкуса. Я предпочитаю "трубы". Подробные различия также обсуждаются в подкасте.