为什么序列[getLine,getLine,getLine]的计算没有被延迟?

时间:2019-04-20 09:01:53

标签: haskell recursion sequence lazy-evaluation

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操作一起使用时,sequenceAsequence一样!   它接受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

2 个答案:

答案 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)

上面的代码使aXaY成为惰性IO操作,并且输出的顺序现在处于+的编译器和库实现的奇想之下。一般来说,这很危险,因此,懒惰IO的unsafe性质。

现在,关于例外。一些仅从环境读取的IO操作,例如getContents是通过惰性IO(unsafeInterleaveIO)实现的。设计人员认为,对于此类读取,惰性IO是可以接受的,并且在许多情况下读取的精确时间并不那么重要。

现在,这是controversial。虽然很方便,但是在许多情况下,惰性IO可能无法预测。例如,我们不知道文件将在何处关闭,而如果我们正在从套接字读取,那可能很重要。我们还需要非常小心,不要过早强制读取:这通常会导致从管道读取时出现死锁。如今,通常首选避免懒惰的IO,并使用诸如pipesconduit之类的库来进行类似“流式”的操作,而不会产生歧义。