了解 Haskell 的懒惰

Understanding Haskell's Laziness

我正在阅读:Haskell interact function

所以我尝试了

interact (unlines . map (show . length) . lines)

它按我预期的那样工作。我输入一些东西,按回车键,然后我在提示符下打印出长度。

然后我想尝试让它简单地重复我输入的内容,所以我尝试了

interact (unlines . map id . lines)

但现在它会重复我输入的每个字符。这是为什么?我认为诀窍在于 lines 后跟 unlines - 但显然不是。 lines "a" 产生 ["a"],那么当我开始输入时第一个函数为什么不立即给出“1”作为输出?显然我对 "Finding the length of a string is not like this -- the whole string must be known before any output can be produced."

有一些误解

这与惰性求值有关。我将尝试以尽可能直观的方式解释这一点。

当你写interact (unlines . map (show . length) . lines)时,每次输入一个字符,我们实际上并不知道下一个输出字符是什么,直到你按下回车。因此,您得到了预期的行为。

但是,在 interact (unlines . map id . lines) = interact id 中的每一点,每次输入一个字符时,都会保证该字符包含在输出中。因此,如果您输入一个字符,该字符也会立即输出。

这是 "lazy" 这个词有点用词不当的原因之一。确实 Haskell 只会在需要时评估某些东西,但另一方面是当需要时,它会尽快这样做。这里 Haskell 需要评估输出,因为你想打印它,所以它会尽可能多地评估它——一次一个字符——具有讽刺意味的是让它看起来很急切!

更具体地说,interact 不是为实时用户输入而设计的——它是为文件输入而设计的,在这种情况下,您可以使用 bash 将文件通过管道传输到可执行文件中。它应该是 运行 像这样的东西:

$ runhaskell Interactor.hs < my_big_file.txt > list_of_lengths.txt

如果你想要 line-by-line 缓冲,你可能必须手动完成,除非你想 'trick' 像 Willem 那样的编译器。下面是一些非常简单的代码,可以按您的预期工作——但请注意,它没有退出状态,这与 interact 不同,它将在 EOF 处终止。

main = do
  ln <- getLine -- Buffers until you press enter
  putStrLn ln   -- Print the line we just got
  main          -- Loop forever

lines "a" 产生 ["a"] 的事实并不意味着如果您当前正在输入 a,那么 lines 只是处理列表 [=12] 的输入=].您应该将输入视为(可能)无限的字符列表。如果提示正在等待用户输入,则下一次输入时 "blocking"。

但是 not 意味着像 lines 这样的函数可以 not 部分解析结果。 lines 以惰性方式实现,因此它处理字符流,每次看到换行符时,它都会开始发出下一个元素。因此,这意味着 lines 可以将无限的字符序列处理成无限的行列表。

但是,如果您使用 length :: Foldable f => f a -> Int,则这需要评估列表(但不是列表的 元素 )。因此,这意味着 length 只会在 lines 开始发出下一个项目时发出答案。

您可以使用 seq(和变体)在完成特定操作之前强制对术语求值。例如 seq :: a -> b -> b 会将第一个参数评估为 弱头部范式 (WHNF),然后 return 第二个参数。

seq的基础上,构建了其他功能,如seqList :: [a] -> [a] in the Data.Lists module of the lists包。

我们可以使用它来推迟评估,直到第一行已知,例如:

-- will echo full lines

import Data.Lists(seqList)

interact (unlines . map (\x -> seqList x x) . lines)