Haskell中的IO效应流

11

考虑以下Haskell程序。我试图以“流式风格”编程,其中函数在流(此处简单地实现为列表)上操作。像normalStreamFunc这样的函数在惰性列表上运作得很好。我可以将无限列表传递给normalStreamFunc,并有效地获得另一个无限列表,但每个值都映射到一个函数上。像effectfulStreamFunc这样的函数效果不太好。IO操作意味着我需要在提取单个值之前对整个列表进行评估。例如,程序的输出是这样的:

a
b
c
d
"[\"a\",\"b\"]"

但我想要的是一种编写 effectfulStreamFunc 的方式,使程序产生以下结果:

a
b
"[\"a\",\"b\"]"

剩余操作未被评估。我可以想象使用unsafePerformIO来解决问题,但我们先不考虑它。以下是程序:

import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = do
    putStrLn x
    rest <- effectfulStreamFunc xs
    return (reverse(x):rest)

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

更新:

感谢大家提供的有用和周到的反馈。我以前没有见过sequence运算符,这对我很有帮助。我曾经考虑过一种(不太优雅)的方法来传递IO(字符串)值而不是字符串,但对于我想要对字符串本身进行操作的编程风格来说,其使用价值有限,因为我希望其他流函数作用于字符串本身,而不是能生成字符串的操作。但是,通过思考其他回复,我认为我明白了为什么这在一般情况下是无法解决的。在我提出的简单情况中,我真正想要的是sequence运算符,因为我认为流的顺序意味着动作的顺序。实际上,并不一定暗示这样的顺序。当我思考一个将两个流作为输入的流函数时(例如,对两个流进行成对相加),如果两个“输入”流都执行了IO,则这些IO操作的顺序是未定义的(除非我们在IO单子中自行定义它)。问题得到解决,感谢大家!

3个回答

7
这段代码怎么样:
import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> [IO (String)]
effectfulStreamFunc [] = []
effectfulStreamFunc (x:xs) =
    let rest = effectfulStreamFunc xs in
        (putStrLn x >> return x) : rest

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     let appliedFns = effectfulStreamFunc fns
     pieces <- sequence $ take 2 appliedFns
     print $ show $ pieces

effectfulStreamFunc并不会实际执行任何IO操作,它只是创建了一个要执行的IO操作列表。(请注意类型签名的更改。)然后主函数会取其中的两个操作,运行它们并打印结果:

a
b
"[\"a\",\"b\"]"

这是因为类型IO(String)只是像其他函数/值一样可以放入列表中,传递等。请注意,在“effectfulStreamFunc”中没有出现“do”语法 - 尽管在其签名中有“IO”,但它实际上是一个纯函数。只有在主函数中运行sequence时才会真正发生效果。

1
如果你仍然喜欢do符号,你可以将effectfulStreamFunc的第二个子句写成:effectfulStreamFunc (x:xs) = let putAction = do putStrLn x return x in putAction : effectfulStreamFunc xs我认为这样读起来更好一些。 - Nathan Shively-Sanders
好观点。我猜do语法与实际运行monad的问题是正交的。 - Jesse Rusak

2

我不是很理解你的主要目标,但是你使用putStrLn会导致整个列表被求值,因为它在执行时会求值参数。建议考虑使用

import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = do
    rest <- effectfulStreamFunc xs
    return (reverse(x):rest)

main :: IO ()
main = do
     let fns = ["a", "b", undefined,"c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

这将导致结果为"[\"a\",\"b\"]",而使用putStrLn版本会导致异常。

1

如Tomh所提到的,你不能真正地“安全地”这样做,因为你正在Haskell中打破引用透明度。你试图懒惰地执行副作用,但是关于懒惰的事情是,你不能保证按什么顺序或者是否对事物进行了评估,所以在Haskell中,当你告诉它执行一个副作用时,它总是被执行,并按照指定的确切顺序。 (即在这种情况下,在return之前对effectfulStreamFunc的递归调用的副作用,因为它们是列出的顺序)你不能在不使用unsafe的情况下懒惰地这样做。

你可以尝试使用类似于unsafeInterleaveIO的东西,这就是延迟IO(例如hGetContents)在Haskell中实现的方式,但它有自己的问题;而你说你不想使用“不安全”的东西。

import System.IO.Unsafe (unsafeInterleaveIO)

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = unsafeInterleaveIO $ do
    putStrLn x
    rest <- effectfulStreamFunc xs
    return (reverse x : rest)

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

在这种情况下,输出看起来像这样

"a
[\"a\"b
,\"b\"]"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接