Haskell 非函数递归或值递归或递归 let 绑定

问题描述

我是 Haskell 的新手,从第一原则开始阅读 Haskell。

在第 10 章第 378 页中,我遇到了这种新语法

fibs = 1 : scanl (+) 1 fibs

似乎 fibs 是一个函数,因为它在右侧被使用或调用

但它的类型是 fibs :: Num a => [a] 所以它是一个

如何在自己的定义中访问值?

如果我确实尝试查看 fibs 中的内容,它只会向无穷大打印。

和其他示例 i = 1 + i 是完全有效的语法,

但是当我在 ghci 中运行它时它也会停止它。

  • 如果不是递归,那是什么递归?
  • 执行流程是什么?
  • i 未被分配时,如何在右侧访问它 全部?
  • 右边第一次 i 的值是多少?
  • 右边 i 的值是多少?

有什么可以帮助的文章吗?

解决方法

在许多(大多数?)语言中,函数是“特殊”值,在语言语法和语义中具有特殊地位。

在 Haskell 中,没有那么多。它们与非函数共享许多属性。特别是:

  • 我们可以将函数(和非函数)作为参数传递给函数。
  • 我们可以从函数返回函数(和非函数)。
  • 我们可以将函数(和非函数)放入列表、对等容器中。
  • 我们可以递归地定义函数(和非函数)。

最后一点如果你不习惯的话可能会令人费解,但没有理由只在用 Haskell 这样的惰性语言定义函数值时才允许递归。

例如,我们可以通过让 1:1:1:.... 来定义无限列表 list = 1 : list。如果你只能理解函数的递归定义,就知道它和list() = 1 : list()很相似,只不过list不是一个函数,而是一个列表。

您也可以尝试理解 1:2:3:4:.... 可以递归定义为 list = 1 : map (+1) list。事实上,展开我们得到的定义:

list
= 1 : map (+1) list
= 1 : map (+1) (1 : map (+1) list)
= 1 : 2 : map (+1) (map (+1) list)
= 1 : 2 : map (+1) (map (+1) (1 : map (+1) list))
= 1 : 2 : 3 : map (+1) (map (+1) (map (+1) list))
...

请注意,上面的定义“有效”,因为它可以在递归需要自己的值之前生成值的一部分。在您提到的 i = i + 1 示例中,这不会发生,因为 i + 1 在可以生成输出的任何部分之前立即需要 i 的值。 (嗯,至少在标准整数类型上是这样。)因此,它具有与我们从无限递归函数(例如 i() = i() + 1)中观察到的行为相同的行为。

,

在 Haskell 中,let 是所谓的 letrec,即在其他语言中的“递归 let”,这意味着 两者名称 在等式中指的是 相同 实体。

这是有道理的,因为等式不是赋值,而是定义。(*)

fibs = 1 : scanl (+) 1 fibs

此处右侧的 fibs 指代与左侧相同的 fibs。它不是被调用,而是被使用。它正在根据其定义使用。它是用惰性数据构造函数 : 定义的。

可以想象评估是通过命名所有临时实体并根据我们已有的内容写下它们的定义来进行的:

print $ take 4 fibs ==>
take 4 fibs =
= take 4 (let {fibs = 1 : scanl (+) 1 fibs} in fibs)
= take 4 (let {fibs = 1 : s1 ; s1 = scanl (+) 1 fibs} in fibs)
= take 4 (let {fibs = 1 : s1 ; s1 = scanl (+) 1 fibs} in (1 : s1))
= take 4 (1 : let {fibs = 1 : s1 ; s1 = scanl (+) 1 fibs} in s1)
= 1 : take 3 (let {fibs = 1 : s1 ; s1 = scanl (+) 1 fibs} in s1)
= 1 : take 3 (let {s1 = scanl (+) 1 (1 : s1)} in s1)
= 1 : take 3 (let {s1 = 1 : scanl (+) 2 s1} in s1)
= 1 : take 3 (let {s1 = 1 : s2; s2 = scanl (+) 2 s1} in (1 : s2))
= 1 : take 3 (1 : let {s1 = 1 : s2; s2 = scanl (+) 2 s1} in s2)
= 1 : 1 : take 2 (let {s1 = 1 : s2; s2 = scanl (+) 2 s1} in s2)
= 1 : 1 : take 2 (let {s2 = scanl (+) 2 (1 : s2)} in s2)
= 1 : 1 : take 2 (let {s2 = 2 : scanl (+) 3 s2} in s2)
= 1 : 1 : take 2 (let {s2 = 2 : s3; s3 = scanl (+) 3 s2} in (2 : s3))
= 1 : 1 : 2 : take 1 (let {s2 = 2 : s3; s3 = scanl (+) 3 s2} in s3)
= 1 : 1 : 2 : take 1 (let {s3 = scanl (+) 3 (2 : s3)} in s3)
= 1 : 1 : 2 : take 1 (let {s3 = 3 : scanl (+) 5 s3} in s3)
= 1 : 1 : 2 : take 1 (let {s3 = 3 : s4; s4 = scanl (+) 5 s3} in (3 : s4))
= 1 : 1 : 2 : 3 : take 0 (let {s3 = 3 : s4; s4 = scanl (+) 5 s3} in s4)
= 1 : 1 : 2 : 3 : [] 
= [1,1,2,3]

事情就是这样。(**)

i 中的 let { i = i+1 } in i 使用严格的 +,因此任何尝试使用 i 循环的值,因为 + 要求两者在返回其值之前它的参数,不像懒惰的:实际上不需要任何参数。


你问过,

如何在自己的定义中访问值?

以上给出了答案,这是双重的。首先,因为(隐含的)let 是递归的,所以等号左边和右边的名称都指的是同一个实体。其次,由于 Haskell 的评估是惰性的,因此没有赋值(也没有通常意义上的“值”)——只是定义。列表不是列表,而是用于计算该列表等的计算过程的定义。


(*) 或者用技术术语来说,是一种懒惰绑定。

(**) 结果是 it's a song,也是一个不错的结果。