【问题标题】:Haskell Lazy ByteString + read/write progress functionHaskell Lazy ByteString + 读/写进度函数
【发布时间】:2011-10-03 20:47:39
【问题描述】:

我正在学习 Haskell Lazy IO。

我正在寻找一种优雅的方式来复制大文件 (8Gb),同时将复制进度打印到控制台。

考虑以下简单的程序,它以静默方式复制文件。

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 Mb 调用一次 -- 不重要

回答:非常感谢 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

【问题讨论】:

  • 顺便说一句,你可以说[from, to] &lt;- getArgs
  • 作为一个类似的问题 - 有没有办法在纯计算上取得进展?
  • @monadic,这是一个类似的问题,但答案却大不相同。基本上,您的函数会返回一个包含所需结果和进度报告的对,进度报告“计算”的方式取决于结果的计算。然后在使用结果之前打印进度报告。如果您想了解更多详细信息,那么我建议您提出一个新问题。

标签: haskell io progress lazy-evaluation bytestring


【解决方案1】:

首先,我想指出,相当多的 Haskell 程序员通常对惰性 IO 持怀疑态度。它在技术上违反了纯度,但在以一致的输入[0] 运行单个程序时,以一种有限的方式(据我所知)并不明显。另一方面,很多人都可以接受它,因为它只涉及一种非常有限的杂质。

为了营造一种实际使用按需 I/O 创建的惰性数据结构的假象,readFile 之类的函数是在幕后使用鬼鬼祟祟的恶作剧实现的。在按需 I/O 中编织是该函数所固有的,并且它不是真正可扩展的,原因几乎与从中获得常规 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 时,它都会延迟 I/O,直到实际使用该值。要在每次发生实际读取时调用您的报告函数,您需要直接将其编入其中,虽然可以编写此类函数的通用版本,但据我所知不存在。

鉴于上述情况,您有几个选择:

  • 查看您正在使用的惰性 I/O 函数的实现,并实现您自己的,包括进度报告功能。如果这感觉像一个肮脏的黑客,那是因为它几乎是,但你去吧。

  • 放弃惰性 I/O 并切换到更明确和可组合的东西。这是整个 Haskell 社区似乎正在朝着的方向,特别是在 Iteratees 上的一些变化,它为您提供了具有更多可预测行为的可组合的小型流处理器构建块。缺点是该概念仍在积极开发中,因此没有就实施或学习使用它们的单一起点达成共识。

  • 放弃惰性 I/O 并切换到普通的旧常规 I/O:编写一个 IO 操作来读取一个块,打印报告信息,并尽可能多地处理输入;然后循环调用它直到完成。根据您对输入的处理以及您在处理中依赖懒惰的程度,这可能涉及从编写几个几乎微不足道的函数到构建一堆有限状态机流处理器并获得 90改造迭代器的百分比。

[0]:这里的底层函数称为unsafeInterleaveIO,据我所知,从中观察杂质的唯一方法需要在不同的输入上运行程序(其中如果它有权以不同的方式表现,它可能只是以在纯代码中没有意义的方式这样做),或者以某些方式更改代码(即,应该没有影响的重构可能会产生非本地影响) .


下面是一个粗略的示例,使用更多可组合的函数以“普通的旧常规 I/O”方式处理事情:

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 sn-ps”目录中的随机源文件上运行它:

$ 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
$

【讨论】:

  • 这是一个了不起的答案。我听过很多关于惰性 IO 的抱怨,但从来没有真正理解过它们;这个解释清楚地表明了为什么一个看起来很好组合的东西实际上基本上是坏的。
  • @Daniel Wagner:目前存在的惰性 I/O 仅以一种方式高度组合:直接用单个输入操作组合纯处理器功能和“幼稚”输出操作。当输出需要知道如何完成读取、何时需要合并输入或处理器函数需要执行 I/O 时,它很快就会变得笨拙。
  • @oshyshko:添加了一个示例。它比您当前的代码长一点(如预期的那样),但您可以希望看到该方法更容易扩展。
  • @oshyshko:两个原因;第一个是,因为我想演示这如何使事情变得更加模块化/可组合,我补充说,因为processBytes 可以在它可能收到一个空的ByteString 的上下文中重复使用,我希望它不采取任何行动那个案子。更准确的原因是我需要再喝一杯咖啡,但当时并没有真正思考清楚。
  • 虽然我要补充一点,如果一个函数可以正确处理所有可能的输入,那么即使您希望它只接收其中的有限子集,这样做也是值得的。如果您以改变预期的方式在其他地方更改程序,则事情不太可能中断。
【解决方案2】:

使用Data.ByteString.Lazy.Progress。它允许您在数据通过时打印各种指标。

【讨论】:

  • 你有任何使用这个包的例子吗?
  • @jinkou2jinkou2:尽管我的 Haskell 技能很差,但我确实设法让它工作。尝试使用trackProgressString "%b" Nothing printLn flowMonad 的效果,其中flowMonad 是执行 IO 的函数。
  • 谢谢。我还问了维护者,他们好心地在包中添加了一个完美的工作示例。
猜你喜欢
  • 1970-01-01
  • 2011-01-31
  • 1970-01-01
  • 1970-01-01
  • 2013-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多