考虑以下Haskell程序。我试图以“流式”编程,其中函数在流上运行(这里简单地作为列表实现)。像normalStreamFunc这样的东西适用于懒惰列表。我可以将无限列表传递给normalStreamFunc并有效地获取另一个无限列表,但是将函数映射到每个值。像effectivefulStreamFunc这样的东西不能很好地工作。 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 monad中对它进行排序来定义它)。问题解决了,谢谢大家!
答案 0 :(得分: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
而不是实际执行任何IO的effectfulStreamFunc,而是创建一个要执行的IO操作列表。 (注意类型签名的更改。)然后main函数接受其中2个操作,运行它们并打印结果:
a
b
"[\"a\",\"b\"]"
这是有效的,因为类型IO (String)
只是一个函数/值,可以放入列表,传递等任何其他函数/值。请注意,在“effectfulStreamFunc”中不会出现do语法 - 它实际上是一个纯函数,尽管它的签名中有“IO”。只有当我们对main中的那些运行sequence
时才会发生效果。
答案 1 :(得分: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版本会导致异常。
答案 2 :(得分:1)
正如Tomh所说,你无法“安全地”做到这一点,因为你打破了Haskell中的引用透明度。你试图懒洋洋地执行副作用,但是关于懒惰的事情是你无法保证以什么顺序或是否对事物进行评估,所以在Haskell中,当你告诉它执行副作用时,它总是被执行,并按照指定的确切顺序。 (即在这种情况下,effectfulStreamFunc
之前return
递归调用的副作用,因为这是他们列出的顺序。你不能在不使用不安全的情况下懒得这样做。
您可以尝试使用类似unsafeInterleaveIO
的内容,这是在Haskell中实现懒惰IO(例如hGetContents
)的方式,但它有自己的问题;而且你说你不想使用“不安全”的东西。
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\"]"