Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 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. Используйте строгий ввод-вывод и загружайте всю строку в память только для ленивого использования ее вашим анализатором. Это просто, предсказуемо, но неэффективно.
  2. Используйте ленивый ввод-вывод и потеряйте способность рассуждать о том, когда ваши производственные эффекты ввода-вывода будут фактически запущены, вызывая возможные утечки ресурсов или исключения закрытого дескриптора в соответствии с графиком потребления проанализированных элементов. Это более эффективно, чем (1), но может легко стать непредсказуемым; или же,
  3. Используйте 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, но он заполнен проблемами и делает приложения подверженными ошибкам. Эти проблемы обсуждаются в подкасте.

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