Haskell怎么有后期评价?

How does Haskell have late evaluation?

听说Haskell评价晚了。然而,无论我尝试做什么,它似乎都以与任何其他编程语言相同的方式对其进行评估;

考虑以下代码:

test :: Bool -> IO()
test n = do
  let y = 5
  print n

main = do
  let y = 8
  test (y == 8)

此代码输出:

True

首先要记住一些重要的一般信息:

Haskell 变量是 不可变的
总是,无一例外,你无能为力。

因此,当您编写类似 let y = 5 的内容时,它 永远不会 更改某些已经存在的变量的值。相反,它引入了一个名为 y 的新变量并为其赋予所需的值。在你的程序的其他地方是否还有一个你碰巧称为 y 的变量是无关紧要的,这完全是一个 different 变量。事实上,考虑一下:

main :: IO ()
main = do
   let y = 1
   do let y = 2
      print y
   print y

输出为

2
1

在您的示例中,let y = 5 语句根本没有效果,很可能会被编译器完全丢弃。事实上,如果你用 -Wall 编译(你应该这样做),GHC 会告诉你:

/tmp/wtmpf-file30239.hs:4:7: warning: [-Wunused-local-binds]
    Defined but not used: ‘y’
  |
4 |   let y = 5
  |       ^

所以特别是,y == 8 检查不可能受到您可以使用的任何 let y = 语句的影响。

事实上,更一般地说,惰性求值不会影响值。这是拥有纯函数式语言的一大优点:因为一切都是不变的,惰性求值通常不会影响值语义,它只会影响完成相同工作的顺序——可以 影响某事终止的速度(或它是否完全终止),但不影响它在完成时产生的价值。在运行时进入 test 函数之前或之后评估 y == 8 并不重要,事实上 Haskell 标准并没有说明任何事情——它只说明了如果 test 最终 甚至不使用 参数值,则非终止参数不应阻止 test 终止(非严格语义).因此,以下演示了惰性求值的实际效果:

unobtanium :: Bool
unobtanium = unobtanium -- infinite loop

don'tTest :: Bool -> IO ()
don'tTest a = do
   putStrLn "Meh, I'm too lazy to do it."

main :: IO ()
main = do
   let y = unobtanium
   don'tTest y

...即使无法评估 y

Why is this code doing early evaluation?

正如@leftaroundabout 所讨论的那样,并非如此。这里的误解是关于变量范围而不是评估。

can I have an example of late evaluation in Haskell?

无限列表

我发现最早容易理解的惰性计算示例是无限列表。考虑典型的斐波那契数列。为了命名起见,一个详细的版本是:

fibs :: [Integer]
fibs = 0 -- First element
     : 1 -- Cons with the second element
     : zipWith (\first second -> first + second) fibs (drop 1 fibs)
         -- Cons with all other elements, dynamically computed.

有些人不喜欢计算。这些人的一个例子是重复列表。考虑一个永无止境的 42 链表......这在 C 中很难以有用的方式做到。

infiniteFortyTwos = 42 :infiniteFortyTwos

错误

错误可能是最常见的惰性求值,但大多数人并没有真正意识到惰性求值的发生。与布尔运算一起使用时通常称为短路,请考虑 shell 脚本:

var=$(ls /dir/that/does/not/exist 2>/dev/null || echo DNE)

这个习语在编程和开发中随处可见。 Perl 早年经常使用|| die "error"。在 Haskell 中,类型不鼓励以这种特定方式混合评估和失败,但惰性功能是相同的。

考虑一下这个构思不佳的例程:

headEquals :: Eq a => a -> [a] -> Bool
headEquals v xs = not (null xs) && v == head xs

或常见的 java 和 C 模式:

if(ptr != NULL && *ptr == value) {
   ...
}

喜结连理

一种更高级的懒惰评估技术是 tying the knot。例如,假设我们要用列表中的最小元素替换列表中的所有元素。让我们假设我们在计算答案时有答案,然后使用返回值作为所述答案:

minList :: [Int] -> [Int]
minList [] = []
minList (m:xs) =
    let (theMin, newList) = go theMin m xs
    in theMin : newList
  where
   go realAnswer partialAnswer []     = (partialAnswer, [])
   go realAnswer partialAnswer (y:ys) =
        let newPartialAnswer = min y partialAnswer
        in (realAnswer :) <$> go realAnswer newPartialAnswer ys