在Haskell中,变量绑定如何与递归一起工作?

在Haskell中,变量绑定如何与递归一起工作?,haskell,functional-programming,Haskell,Functional Programming,我读过关于Haskell的书,书中说,变量一旦绑定到表达式上,就不能恢复 x = 10 x = 11 Assign.hs:2:1: error: Multiple declarations of ‘x’ Declared at: Assign.hs:1:1 Assign.hs:2:1 drop n xs = if n <= 0 || null xs then xs else dro

我读过关于Haskell的书,书中说,变量一旦绑定到表达式上,就不能恢复

x = 10
x = 11

Assign.hs:2:1: error:
    Multiple declarations of ‘x’
    Declared at: Assign.hs:1:1
                 Assign.hs:2:1
drop n xs = if n <= 0 || null xs
              then xs
              else drop (n-1) (tail xs)
如果是这样的话。。。当同一个变量不断绑定到其他事物时,递归是如何工作的?比如说

x = 10
x = 11

Assign.hs:2:1: error:
    Multiple declarations of ‘x’
    Declared at: Assign.hs:1:1
                 Assign.hs:2:1
drop n xs = if n <= 0 || null xs
              then xs
              else drop (n-1) (tail xs)
drop nxs=if nHaskell有词法作用域。在作用域内声明变量时,它会对外部作用域隐藏任何同名变量。例如,在下面的程序中

x :: Int
x = 5

main :: IO ()
main = do
         x <- readLn :: IO Int
         print x
因此
x
外部
main
main
中的
x
是不同的变量,内部范围中的变量临时隐藏外部范围中的变量。(好的,在本例中,“暂时”的意思是直到
main
返回,这是程序运行的时间,但您得到了这个概念。)

类似地,当您声明
drop n xs
\n xs->
时,
n
xs
是仅在调用函数期间存在的局部变量。可以使用不同的
n
xs
值多次调用它

当函数是尾部递归的,即返回对自身的调用时,编译器知道它将用相同类型的更新参数替换旧参数,这些参数来自不再存在的作用域。因此,它可以重用堆栈帧,并将新参数存储在与以前参数相同的位置。生成的代码可以与过程语言中的迭代一样快。

From

在给定范围内,Haskell中的变量只定义一次,不能更改

但是作用域不仅仅是函数的代码。粗略地说,它是函数的代码+它的上下文,即调用它的参数,以及它外部作用域中的任何内容。这通常被称为闭包。每次调用函数时,代码都在新的闭包下运行,因此变量可以计算为不同的值


递归情况在这方面并没有什么特别之处:被其他代码调用两次的函数将以不同的闭包运行,其内部变量可以计算为不同的东西。

在几乎任何语言中,标识符只有在作用域中才有意义;概念上是一种从名称到它们所表示的事物的隐式映射

在现代语言中,通常(至少)有一个全局作用域,每个局部作用域与每个函数/过程关联。伪代码示例:

x = 1
print x
x = 2
print x

function plus_one(a):
  b = 1
  return a + b

print plus_one(x)
x
是全局范围内的名称,
a
b
是函数的局部范围内的名称
加上一个

命令式语言(和非纯声明性语言)通常是通过将这些名称映射到一种插槽或鸽子洞来理解的,在这个插槽或鸽子洞中可以通过赋值来存储内容,而当前连接则通过使用该名称来引用。这之所以有效,是因为对这些项目的一种必要的、循序渐进的思考方式使我们能够理解“当前”的含义。1上面的例子说明了这一点<代码>x
首先分配
1
,然后打印,然后分配
2
,然后再次打印;我们希望它先打印“1”,然后打印“2”(然后从最后一行打印“3”)

如果将变量理解为可以存储东西的插槽,那么很容易陷入这样的陷阱:将表示函数参数的局部变量看作是在调用函数时填充的插槽。我之所以说陷阱,是因为这不是思考函数参数和调用的有用方法,即使在命令式语言中也是如此。只要每个函数一次只有一个“正在运行”的调用,它就会或多或少地起作用,但引入任意数量的通用编程特性之一会破坏这种模型(递归、并发、惰性、闭包等)。这就是我在这里看到的许多问题的核心误解,海报在理解递归调用方面有困难,或者想知道如何从外部访问函数的局部变量,等等

实际上,您应该将函数视为具有与该函数的每个调用相关联的单独作用域2。函数本身有点像作用域的模板,而不是作用域本身(尽管公共语言通常将“函数的作用域”称为速记)。如果您为函数的参数提供了绑定,那么您可以生成一个作用域,但是一个典型的函数会使用不同的参数多次调用,因此该函数不只是一个作用域

考虑我的伪代码
加上一个
,带有参数
a
。您可以想象
a
是变量的本地名称,调用
plus one(x)
只需将
x
的内容分配到
a
的插槽中,然后开始执行
plus one
的代码。但我认为最好是这样想,当你在
x
上调用
加上一个
时,你正在创建一个新的作用域,其中有一个名为
a
的变量(此时包含全局作用域
x
的内容),但它不是“the”
a
变量

这对于理解递归非常重要,即使在命令式语言中也是如此:

function drop(n, xs):
  if n <= 0 || null(xs):
    return xs
  else:
    return drop(n - 1, tail(xs))
现在我们在递归调用之后使用
xs
。如果我们想象只有一个
xs
,那么这很难解释,但是如果我们每次调用
drop
时,在一个单独的作用域中都有一个单独的
xs
,那么这很难解释。当我们递归调用
drop
(传递它
n-1
tail(xs)
)时,它会创建自己独立的
xs
,因此在这个范围内
print xs
仍然可以访问
xs
,这是完全不神秘的

那么,是t吗