main = do
input <- sequence [getLine, getLine, getLine]
mapM_ print input
让我们看看该程序的实际作用:
m@m-X555LJ:~$ runhaskell wtf.hs
asdf
jkl
powe
"asdf"
"jkl"
"powe"
令我惊讶的是,这里似乎没有懒惰。取而代之的是,对3个getLine
进行急切的评估,将读取的值存储在内存中,然后(而不是之前)全部打印出来。
比较:
main = do
input <- fmap lines getContents
mapM_ print input
让我们看看这一点:
m@m-X555LJ:~$ runhaskell wtf.hs
asdf
"asdf"
lkj
"lkj"
power
"power"
完全不同的东西。行被一一读取并一一打印。这对我来说很奇怪,因为我看不到这两个程序之间有什么区别。
来自LearnYouAHaskell:
与I / O操作一起使用时,
sequenceA
与sequence
一样! 它接受I / O操作的列表,并返回一个I / O操作,该操作将 执行这些操作中的每一个,并在其结果中列出 这些I / O动作的结果。那是因为要设置[IO a]
的值 转换为IO [a]
值,以进行I / O操作,产生一个列表 结果执行时,所有这些I / O操作都必须排序,这样 然后当评估是 被迫。如果不执行,就无法获得I / O操作的结果 它。
我很困惑。我不需要执行所有IO操作就可以得到一个结果。
本书前面几段显示了sequence
的定义:
sequenceA :: (Applicative f) => [f a] -> f [a] sequenceA [] = pure [] sequenceA (x:xs) = (:) <$> x <*> sequenceA xs
很好的递归;这里没有什么暗示我这个递归不应该是懒惰的;就像在其他任何递归中一样,要使Haskell成为返回列表的头,不必执行所有递归步骤!
比较:
rec :: Int -> [Int]
rec n = n:(rec (n+1))
main = print (head (rec 5))
实际情况:
m@m-X555LJ:~$ runhaskell wtf.hs
5
m@m-X555LJ:~$
很显然,这里的递归是懒惰的,而不是急切的。
那么为什么sequence [getLine, getLine, getLine]
示例中的递归会急切地执行?
关于为什么重要的是,按顺序运行IO操作 不管结果如何:想象一个动作
createFile :: IO ()
和writeToFile :: IO ()
。当我做一个sequence [createFile, writeToFile]
时,我希望他们既完成又按顺序完成,甚至 尽管我不在乎他们的实际结果( 毫无价值的()
)!
我不确定这如何适用于此问题。
也许我会这样写我的Q ...
在我看来:
do
input <- sequence [getLine, getLine, getLine]
mapM_ print input
应该贬义为这样的东西:
do
input <- do
input <- concat ( map (fmap (:[])) [getLine, getLine, getLine] )
return input
mapM_ print input
反过来,应该反斜率化为这样的东西(伪代码,很抱歉):
do
[ perform print on the result of getLine,
perform print on the result of getLine,
perform print on the result of getLine
] and discard the results of those prints since print was applied with mapM_ which discards the results unlike mapM
答案 0 :(得分:6)
getContents
很懒,getLine
不是。惰性IO本身不是Haskell的功能,它是某些特定IO操作的功能。
我很困惑。我不需要执行所有IO操作就可以得到一个结果。
是的!这是IO
最重要的功能之一,如果您写a >> b
或同等水平的话,
do a
b
然后,您可以确定a
肯定在b
之前“运行”(请参阅脚注)。 getContents
实际上是相同的,它先运行然后运行……但是返回的结果是一个偷偷摸摸的结果,偷偷地做了 more 当您尝试对其进行评估时。 那实际上是令人惊讶的一点,它在实践中可能会导致一些非常有趣的结果(例如,您正在读取的文件正在被删除或更改的内容)重新处理getContents
的结果,因此在实际程序中您可能不应该使用它,它主要是为方便起见,在您不关心此类事情的程序中(代码高尔夫,一次性脚本或实例)。
关于为什么,重要的是,无论结果如何,依次执行IO操作:想象一个操作createFile :: IO ()
和writeToFile :: IO ()
。当我执行sequence [createFile, writeToFile]
时,我希望它们既完成又井井有条,即使我不在乎它们的实际结果(两者都是非常无聊的价值,{ {1}})!
解决修改问题
应该贬义为这样的东西:
()
不,实际上是这样的:
do
input <- do
input <- concat ( map (fmap (:[])) [getLine, getLine, getLine] )
return input
mapM_ print input
(do
input <- do
x <- getLine
y <- getLine
z <- getLine
return [x,y,z]
mapM_ print input
的实际定义或多或少是这样的:
sequence
答案 1 :(得分:4)
从技术上讲,
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs
我们找到<*>
,它首先在左侧运行操作,然后在右侧运行操作,最后将它们的结果一起应用。这就是使列表中的第一个效果首先出现的原因,依此类推。
实际上,在单子上,f <*> x
等于
do theF <- f
theX <- x
return (theF theX)
通常,请注意,所有IO操作通常都是按顺序执行,从头到尾(请参阅下面的一些罕见例外)。对于程序员而言,以完全懒惰的方式进行IO将是一场噩梦。例如,考虑:
do let aX = print "x" >> return 4
aY = print "y" >> return 10
x <- aX
y <- aY
print (x+y)
Haskell保证输出为x y 14
(按此顺序)。如果我们有完全懒惰的IO,则还可以得到y x 14
,这取决于+
首先强制使用哪个参数。在这种情况下,我们将需要确切地知道每个操作要求懒惰的重击的顺序,这是程序员绝对不想关心的。在这种详细的语义下,x + y
不再等效于y + x
,在许多情况下破坏了公式推理。
现在,如果我们想强制IO变得懒惰,我们可以使用其中一种禁止的功能,例如
do let aX = unsafeInterleaveIO (print "x" >> return 4)
aY = unsafeInterleaveIO (print "y" >> return 10)
x <- aX
y <- aY
print (x+y)
上面的代码使aX
和aY
成为惰性IO操作,并且输出的顺序现在处于+
的编译器和库实现的奇想之下。一般来说,这很危险,因此,懒惰IO的unsafe
性质。
现在,关于例外。一些仅从环境读取的IO操作,例如getContents
是通过惰性IO(unsafeInterleaveIO
)实现的。设计人员认为,对于此类读取,惰性IO是可以接受的,并且在许多情况下读取的精确时间并不那么重要。
现在,这是controversial。虽然很方便,但是在许多情况下,惰性IO可能无法预测。例如,我们不知道文件将在何处关闭,而如果我们正在从套接字读取,那可能很重要。我们还需要非常小心,不要过早强制读取:这通常会导致从管道读取时出现死锁。如今,通常首选避免懒惰的IO,并使用诸如pipes
或conduit
之类的库来进行类似“流式”的操作,而不会产生歧义。