Haskell Lazy ByteString + функция чтения / записи прогресса - PullRequest
20 голосов
/ 12 июля 2011

Я слушаю Haskell Lazy IO.

Я ищу элегантный способ скопировать большой файл (8 ГБ) во время печати процесса копирования на консоль.

Рассмотрим следующую простую программу, которая автоматически копирует файл.

module Main where

import System
import qualified Data.ByteString.Lazy as B

main = do [from, to] <- getArgs
          body <- B.readFile from
          B.writeFile to body

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

onReadBytes :: Integer -> IO ()
onReadBytes count = putStrLn $ "Bytes read: " ++ (show count)

ВОПРОС: как встроить функцию onReadBytes в Lazy ByteString, чтобы она вызывалась при успешном чтении? Или, если этот дизайн не очень хороший, то каков способ сделать это на Haskell?

ПРИМЕЧАНИЕ: частота обратного вызова не важна, ее можно вызывать каждые 1024 байта или каждые 1 Мб - не важно

ОТВЕТ: Большое спасибо Camccann за ответ. Я предлагаю прочитать это полностью.

Ниже приведена моя версия кода, основанная на коде camccann, вы можете найти его полезным.

module Main where

import System
import System.IO
import qualified Data.ByteString.Lazy as B

main = do [from, to] <- getArgs
          withFile from ReadMode $ \fromH ->
            withFile to WriteMode $ \toH ->
              copyH fromH toH $ \x -> putStrLn $ "Bytes copied: " ++ show x

copyH :: Handle -> Handle -> (Integer -> IO()) -> IO ()
copyH fromH toH onProgress =
    copy (B.hGet fromH (256 * 1024)) (write toH) B.null onProgress
    where write o x  = do B.hPut o x
                          return . fromIntegral $ B.length x

copy :: (Monad m) => m a -> (a -> m Integer) -> (a -> Bool) -> (Integer -> m()) -> m()
copy = copy_ 0

copy_ :: (Monad m) => Integer -> m a -> (a -> m Integer) -> (a -> Bool) -> (Integer -> m()) -> m()
copy_ count inp outp done onProgress = do x <- inp
                                          unless (done x) $
                                            do n <- outp x
                                               onProgress (n + count)
                                               copy_ (n + count) inp outp done onProgress

Ответы [ 2 ]

25 голосов
/ 12 июля 2011

Во-первых, я хотел бы отметить, что довольно много программистов на Haskell относятся к ленивому IO в целом с некоторым подозрением.Технически это нарушает чистоту, но ограниченным образом, что (насколько я знаю) не заметно при запуске единой программы на согласованном вводе [0] .С другой стороны, многим людям это хорошо, опять же, потому что это связано только с очень ограниченным видом нечистоты.

Чтобы создать иллюзию ленивой структуры данных, которая фактически создается с помощью ввода-вывода по требованию, такие функции, как readFile, реализованы с использованием скрытых махинаций за кулисами.Плетение при вводе / выводе по требованию присуще этой функции, и оно на самом деле не расширяемо по тем же причинам, по которым иллюзия получения регулярного ByteString от него убедительна.

Ручная обработка деталейи написание псевдокода, что-то вроде readFile в основном работает так:

lazyInput inp = lazyIO (lazyInput' inp)
lazyInput' inp = do x <- readFrom inp
                    if (endOfInput inp)
                        then return []
                        else do xs <- lazyInput inp
                                return (x:xs)

... где каждый раз, когда вызывается lazyIO, он откладывает ввод / вывод до фактического использования значения.Чтобы вызывать вашу функцию отчетности каждый раз, когда происходит фактическое чтение, вам нужно было бы встроить ее напрямую, и хотя обобщенную версию такой функции можно было бы написать, насколько мне известно, ее не существует.

Учитывая вышеизложенноеУ вас есть несколько вариантов:

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

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

  • Откажитесь от ленивого ввода-вывода и переключитесь на обычный старый обычныйВвод / вывод: напишите действие IO, которое читает блок, печатает отчетную информацию и обрабатывает столько данных, сколько может;затем вызвать его в цикле, пока не будет сделано.В зависимости от того, что вы делаете с вводом, и от того, насколько сильно вы полагаетесь на лень в своей обработке, это может включать в себя что угодно - от написания пары почти тривиальных функций до построения группы потоковых процессоров с конечным числом машин и получения 90% пути к повторному изобретению Итераторов.

[0] : базовая функция здесь называется unsafeInterleaveIO, и, насколько мне известно, единственные способыдля наблюдения за примесями требуется либо запустить программу на другом входе (в этом случае она имеет право вести себя по-разному в любом случае, это может быть сделано так, что это не имеет смысла в чистом коде), либо изменить код определенным образом(т.е. рефакторинги, которые не должны иметь эффекта, могут иметь нелокальные эффекты).


Вот пример грубого выполнения «обычного старого обычного ввода-вывода» с использованием более комбинируемых функций:

import System
import System.IO
import qualified Data.ByteString.Lazy as B

main = do [from, to] <- getArgs
          -- withFile closes the handle for us after the action completes
          withFile from ReadMode $ \inH ->
            withFile to WriteMode $ \outH ->
                -- run the loop with the appropriate actions
                runloop (B.hGet inH 128) (processBytes outH) B.null

-- note the very generic type; this is useful, because it proves that the
-- runloop function can only execute what it's given, not do anything else
-- behind our backs.
runloop :: (Monad m) => m a -> (a -> m ()) -> (a -> Bool) -> m ()
runloop inp outp done = do x <- inp
                           if done x
                             then return ()
                             else do outp x
                                     runloop inp outp done

-- write the output and report progress to stdout. note that this can be easily
-- modified, or composed with other output functions.
processBytes :: Handle -> B.ByteString -> IO ()
processBytes h bs | B.null bs = return ()
                  | otherwise = do onReadBytes (fromIntegral $ B.length bs)
                                   B.hPut h bs

onReadBytes :: Integer -> IO ()
onReadBytes count = putStrLn $ "Bytes read: " ++ (show count)

«128» - это сколькобайт для чтения за один раз.Выполнение этого в произвольном исходном файле в моей директории "Stack Overflow snippets":

$ runhaskell ReadBStr.hs Corec.hs temp
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 128
Bytes read: 83
$
2 голосов
/ 07 ноября 2011

Используйте Data.ByteString.Lazy.Progress .Он позволяет печатать все виды метрик при прохождении данных.

...