如何让Haskell的IO函数尽早输出每一项处理结果?
我构建了一系列处理“步骤”,旨在尽可能早地输出信息。例如:
mainFromSettings :: Settings -> IO () ... mainFromSettings ... = do Sys.setInputEcho False Sys.hSetBuffering Sys.stdout Sys.NoBuffering sContent <- getContents let records :: [[String]] records = Rcr.fromContent rcrConfig sContent ... print records
上述代码每次从惰性IO流sContent生成一条记录后就会打印它。我认为这是因为print records会在输入一行后就获取已准备好的列表头部进行打印,但如果列表顺序反转可能就无法生效。
但其中一个处理步骤无法实现该效果:必须等所有记录处理完成后才会输出内容,差异在于该步骤包含一个为每条记录补充额外信息的IO函数。为复现问题,我编写了模拟该IO函数的代码:
import qualified System.IO as Sys import qualified System.IO.Echo as Sys main :: IO () main = Sys.setInputEcho False >> Sys.hSetBuffering Sys.stdout Sys.NoBuffering >> Sys.hSetBuffering Sys.stdin Sys.NoBuffering >> checkAll ["12","34","56"] >>= print checkAll :: [String] -> IO [String] checkAll [] = return [] checkAll (s:lrs) = do l <- getLine let symbol = if l == s then "==" else "/=" p1 = l ++ symbol ++ s p2 <- checkAll lrs return (p1 : p2)
这段代码需等三行输入完成后才会输出结果。我还尝试了foldrM的实现:
import qualified System.IO as Sys import qualified System.IO.Echo as Sys import qualified Data.Foldable as Fld main :: IO () main = Sys.setInputEcho False >> Sys.hSetBuffering Sys.stdout Sys.NoBuffering >> Sys.hSetBuffering Sys.stdin Sys.NoBuffering >> checkAll [] ["12","34","56"] >>= print checkAll :: [String] -> [String] -> IO [String] checkAll = Fld.foldrM f where f :: String -> [String] -> IO [String] f s ls = do l <- getLine if l == s then return (ls ++ [l ++ "==" ++ s]) else return (ls ++ [l ++ "/=" ++ s])
同样需要等全部输入完成才输出。但移除中间的IO函数后:
import qualified System.IO as Sys import qualified System.IO.Echo as Sys main :: IO () main = Sys.setInputEcho False >> Sys.hSetBuffering Sys.stdout Sys.NoBuffering >> Sys.hSetBuffering Sys.stdin Sys.NoBuffering >> getContents >>= print . lines
就能正常实现逐条输出的预期效果。
请问如何实现逐条尽早输出的效果?这种惰性IO方案是否可行?是否需要使用conduits来实现?我已参考过《Haskell - Read Lines from Handle without blocking》这一主题。
1. 问题根源
你的checkAll函数是严格遍历整个列表后才返回完整结果,print要等整个IO列表构造完成才能输出。而惰性IO版本能逐条输出,是因为getContents生成的是惰性字符串,lines和Rcr.fromContent都是纯函数,print处理列表时会按需求值,拿到一条就输出一条。
但一旦引入每条记录都需要IO操作的逻辑,纯惰性列表的模式就失效了——因为IO [a]类型是先完成所有IO再返回列表,无法做到边做IO边输出。
2. 实现逐条输出的方法
方案一:边处理边输出(最直接)
把输出逻辑嵌入到每条记录的IO处理中,不需要先收集所有结果再打印:
import qualified System.IO as Sys import qualified System.IO.Echo as Sys main :: IO () main = do Sys.setInputEcho False Sys.hSetBuffering Sys.stdout Sys.NoBuffering Sys.hSetBuffering Sys.stdin Sys.NoBuffering mapM_ checkAndPrint ["12","34","56"] checkAndPrint :: String -> IO () checkAndPrint s = do l <- getLine let symbol = if l == s then "==" else "/=" result = l ++ symbol ++ s print result
mapM_会遍历列表,对每个元素执行checkAndPrint,处理完一条就立刻输出,完全不需要等待所有输入。
方案二:用惰性IO包装带IO的处理(谨慎使用)
如果必须保留“生成结果列表”的模式,可以用unsafeInterleaveIO实现惰性IO——它能让IO操作延迟到列表元素被求值时才执行:
import qualified System.IO as Sys import qualified System.IO.Echo as Sys import System.IO.Unsafe (unsafeInterleaveIO) main :: IO () main = do Sys.setInputEcho False Sys.hSetBuffering Sys.stdout Sys.NoBuffering Sys.hSetBuffering Sys.stdin Sys.NoBuffering results <- checkAll ["12","34","56"] print results checkAll :: [String] -> IO [String] checkAll [] = return [] checkAll (s:lrs) = do l <- getLine let symbol = if l == s then "==" else "/=" p1 = l ++ symbol ++ s p2 <- unsafeInterleaveIO $ checkAll lrs return (p1 : p2)
注意:unsafeInterleaveIO是不安全操作,可能导致IO执行顺序不可预测,仅在明确知道风险时使用。
3. 关于惰性IO和conduit的选择
- 你的场景中,惰性IO可行但有局限性:纯处理逻辑(无每条记录的IO)用惰性IO很顺畅,但涉及每条记录的IO时,需要借助
unsafeInterleaveIO,风险较高。 - 如果是更复杂的流式处理(比如资源管理、错误处理、双向流),conduit/pipe等流式库更合适,它们能在保证安全的前提下实现边处理边输出,避免惰性IO的陷阱。
内容的提问来源于stack exchange,提问作者Jörg Brüggmann

