我只是在学习haskell,我想做一个非常简单的程序,当有空行时,它会获取一个大文件并将其拆分为多个文件。
Line 1: skdjsakjadsldas
Line 2: sadjkndasjkdsajk
Line 3: sadojdadjisod
Line 4:
Line 5: asdjdashjkda
Line 6: asdiadsjidji
上面的文件将变成两个文件,一个包含前3行,另一个包含最后2行。为了做到这一点,我做了以下功能:
consumeLines :: Handle -> Handle -> IO ()
consumeLines handleRead handleWrite = do
result <- hIsEOF handleRead
unless result $ do
contents <- hGetLine handleRead
when (contents /= "") $ do
hPutStrLn handleWrite contents
consumeLines handleRead handleWrite
这个想法是给一个Handler提供我在文件中的位置,给另一个Handler一个我要写当前信息的文件。我的问题是,是否有更聪明的方法可以做到这一点?我有点害怕使用hGetContents
和其他方法,因为我可能会把整个文件加载到我的机器中。这有点令人费解,因为在每一步我都必须创建一个新文件来写入行。有什么想法吗?
在Haskell中解决此问题的传统方法是:
-
加载输入文件
-
将输入拆分为行
-
按空行将行分组为段落
-
为每个段落生成一个文件名
-
将每个段落的行写入相应的文件
这是一个适合懒惰I/O的问题。对于#1,您可以简单地使用readFile
,它生成一个字符串,当您遍历文件时,该字符串将按需延迟读取文件:
getParagraphs :: FilePath -> IO [String]
getParagraphs path = do
input <- readFile inputHandle
-- …
我有点害怕使用
hGetContents
和其他方法,因为我可能会把整个文件加载到我的机器中。
这是一个合理的担忧!如果您的代码保留了对readFile
或hGetContents
的整个输入字符串的引用,那么它将把整个文件保存在内存中,而不仅仅是保留必要的部分。这是在较大的程序中避免懒惰I/O的一个原因,在那里更容易犯错误,但这里的程序足够小,我们可以很容易地验证它
对于#2和#3,将文件拆分为行和段,然后可以使用纯lines
和groupBy
函数!
import Data.Function (on)
import Data.List (groupBy)
getParagraphs :: FilePath -> IO [String]
getParagraphs path = do
input <- readFile path
pure $ splitParagraphs input
-- Or: getParagraphs path = splitParagraphs <$> readFile path
-- Or: getParagraphs path = fmap splitParagraphs (readFile path)
-- Or: getParagraphs = fmap splitParagraphs . readFile
splitParagraphs :: String -> [String]
splitParagraphs
= map unlines -- Concatenate back into paragraphs
. filter (not . any null) -- Remove runs containing empty lines
. groupBy ((==) `on` null) -- Group empty and non-empty lines
. lines -- Split into lines
一般来说,将I/O限制在程序中做…好吧,输入和输出的部分被认为是一种好的风格!大多数实际计算都可以用纯代码完成。
然后,您可以单独生成要向其中写入段落的文件路径列表(#4(,并使用for_
遍历为每个路径调用writeFile
(#5(:
import Data.Foldable (for_)
writeFiles :: FilePath -> Int -> String -> [String] -> IO ()
writeFiles prefix startIndex extension contents = do
for_ (zip paths contents) $ (path, content) -> do
writeFile path content
where
paths = map makePath [startIndex ..]
makePath i = concat [prefix, show i, ".", extension]
或者更好的是,您可以使用一元zipWith
,特别是zipWithM_
来避免累积结果列表,因为writeFile
操作都返回伪单位()
值:
import Control.Monad (zipWithM_)
-- …
writeFiles prefix startIndex extension contents
= zipWithM_ writeFile paths contents
where
-- …
然后,您可以在主程序中简单地将这些连接起来。
main :: IO ()
main = do
paragraphs <- readParagraphs "input.txt"
writeFiles "output" 1 "txt" paragraphs -- output1.txt, output2.txt, &c.
-- Or: readParagraphs "input.txt" >>= writeFiles "output" 1 "txt"
(我刚刚使用了FilePath
和常量文件进行说明;将其调整为使用Handle
s和动态输入非常简单。(
在这种情况下,您可以确信readFile
的结果不会被不必要地保留,因为它的每个使用者splitParagraphs
和writeFiles
只包含对输入的单个线性遍历。
然而,在更复杂的情况下,惰性I/O可能会使您很容易犯资源错误,即您保留文件内容的时间超过了必要的时间,或者保持文件Handle
的打开时间超过了需要的时间,更糟糕的是,在实际使用完Handle
之前,不经意地关闭了它!
这些问题的一般解决方案可在具有资源意识的流式传输包中找到,如管道、导管和流式传输。这些库可以帮助您保证程序的资源使用,同时仍然以方便的方式交织I/O效果和纯数据处理。然而,探索每一个图书馆本身就是一个答案。
我将从以下内容开始:
main = readFile path >>= return . zip [1..] . splitFile >>= writeNthFile path
然后只需实现splitFile和writeNthFile。