在这个link,提到了函数式编程。具体来说,作者说:
同时意味着我们假设lambda演算中的一个语句一次被评估。琐碎的功能:
λf(x) ::= x f(x)
定义了为x插入的无限序列。逐步扩展看起来像这样:
0 - f(x)
1 - x f(x)
2 - x x f(x)
3 - x x x f(x)
关键在于我们必须假设步骤300万中的'f()'和'x'具有与第一步中相同的含义。
此时,那些了解FP的人正在集体呼吸下嘀咕“参照透明度”。我知道。我会在一分钟内打败它。现在,只要暂停你的怀疑就足以承认约束确实存在,并且土豚不会受到伤害。
在现实世界的计算机中无限扩展的问题是......好吧......它们是无限的。如同,“无限循环”无限。在进行下一次评估之前,您无法评估无限序列的每个术语,除非您计划在等待答案时花费很长时间的咖啡休息时间。
幸运的是,理论逻辑得到了拯救,并告诉我们预订评估总会给我们提供与后序评估相同的结果。
更多的词汇..需要另外一个功能..幸运的是,这是一个简单的:
λg(x) ::= x x
现在......当我们发表声明时:
g(f(x))
预购评估说我们必须在将其插入g()之前完全扩展f(x)。但这需要永远,这是......不方便。后序评估说我们可以这样做:
0 - g(f(x))
1 - f(x) f(x)
2 - x f(x) x f(x)
3 - x x f(x) x x f(x)
。 。 。有人可以向我解释这里的意思吗?我不知道所说的是什么。也许请指出一个非常好的FP入门,让我开始。
答案 0 :(得分:1)
(警告,这个答案非常冗长。我认为最好包括lambda演算的一般知识,因为几乎不可能找到它的好解释)
作者似乎使用语法λg(x)
来表示名为的函数,而不是lambda演算中的传统函数。作者也似乎正在详细讨论lambda演算如何不是函数式编程,就像图灵机不是命令式编程一样。在那些经常用于表示它们的编程语言中没有出现的抽象中存在着实用性和理想。但在进入之前,关于lambda演算的入门书可能有所帮助。在lambda演算中,所有函数都如下所示:
λarg.body
那就是它。有一个λ
符号(称为" lambda"因此得名)后跟一个命名参数,只有一个命名参数,然后是一段时间,然后是表示函数体的表达式。例如,identity
函数接受任何东西并将其直接返回将看起来像这样:
λx.x
评估表达式只是一系列简单的规则,用于将函数和参数与其体表达式进行交换。表达式的格式为:
function-or-expression arg-or-expression
减少它通常有规则"如果左边的东西是表达式,减少它。否则,它必须是函数,因此使用arg-or-expression
作为函数的参数,并将此表达式替换为函数的body
。非常重要的是要注意无要求在将用作参数之前将arg-or-expression
缩小。也就是说,以下两者在表达式λx.x (λy.y 0)
中是等价的且在数学上相同的减少(假设你有一些0的定义,因为lambda演算要求你将数字定义为函数):
λx.x (λy.y 0)
=> λx.x 0
=> 0
λx.x (λy.y 0)
=> λy.y 0
=> 0
在第一次缩减中,参数在<{em>}用于λx.x
函数之前减少了。在第二个中,参数仅仅是替换到λx.x
函数体中 - 在使用之前它没有被减少。当这个概念用于编程时,它被称为&#34;懒惰评估&#34; - 在您需要之前,您实际上并未评估(减少)表达式。值得注意的是,在lambda演算中,在替换之前参数是否减少无关紧要。 lambda演算的数学证明只要两者都终止,你就会得到相同的结果。在编程语言中绝对不是这种情况,因为各种各样的事情(通常与程序状态的变化有关)会使懒惰评估与正常评估不同。
Lambda演算需要一些扩展才有用。没有办法说出事情。假设我们允许这样做。特别是,让我们自己定义lambda演算中函数的含义:
λname(arg).body
我们说这意味着函数λarg.body
绑定到name
,并且任何随附的lambda表达式中的任何其他位置我们都可以替换{{1}与name
。所以我们可以这样做:
λarg.body
现在,当我们写λidentity(x).x
时,我们只会将其替换为identity
。然而,这引入了一个问题。如果命名函数引用自身会发生什么?
λx.x
现在我们遇到了问题。根据我们的规则,我们应该能够用λevil(x).(evil x)
中的evil
替换名称绑定的内容。但是,由于名称必须body
,一旦我们尝试:
λx.(evil x)
我们得到一个无限循环。我们永远不会评估这个表达式,因为我们无法将它从我们特殊的命名lambda形式转换为常规的lambda表达式。我们无法通过我们的特殊扩展语言转到常规的lambda演算,因为我们无法满足&#34的规则;用函数表达式λevil(x).(evil x)
=> λevil(x).(λx.(evil x) x)
=> λevil(x).(λx.(λx.(evil x) x) x)
=> ...
替换evil
是绑定到#34;。有一些技巧可以解决这个问题,但我们会在一分钟内解决这个问题。
这里重要的一点是,这与完全不同于普通的lambda演算程序,该程序无限评估并且永不完成。例如,考虑自我应用函数,它取一些东西并将其应用于自身:
evil
如果我们使用λx.(x x)
函数对此进行评估,我们会得到:
identity
使用命名函数并命名此函数λx.(x x) λx.x
=> λx.x λx.x
=> λx.x
:
self
但是如果我们将self identity
=> identity identity
=> identity
传递给本身会发生什么?
self
我们得到一个表达式,循环反复将λx.(x x) λx.(x x)
=> λx.(x x) λx.(x x)
=> λx.(x x) λx.(x x)
=> ...
反复减少到self self
。这是一个普通的无限循环,你可以在任何(图灵完备)编程语言中找到它。
这与递归定义问题的区别在于我们的名称和定义不是lambda calculus 。它们是 shorthands ,我们可以通过遵循一些规则扩展到lambda演算。但是在self self
的情况下,我们无法将其扩展为lambda演算,因此我们甚至不能运行lambda演算表达式。我们的命名函数&#34;无法编译&#34;从某种意义上说,类似于将编程语言编译器发送到无限循环时,您的代码甚至从未启动,而不是实际的运行时循环时。 (是的,它是entirely possible使编译器陷入无限循环。)
有一些非常聪明的方法来解决这个问题,其中一个是臭名昭着的Y-combinator。基本的想法是你把我们有问题的λevil(x).(evil x)
函数改为而不是接受一个参数并尝试递归,接受一个参数而返回另一个接受参数的函数,所以你的evil
表达式有两个可用的参数:
body
如果我们评估λevil(f).λy.(f y)
,我们会得到一个新函数,它接受一个参数并只用它调用evil identity
。以下评估首先使用 - &gt;显示名称替换,然后使用=&gt;:
identity
如果我们将(evil identity) 0
-> (λf.λy.(f y) identity) 0
-> (λf.λy.(f y) λx.x) 0
=> λy.(λx.x y) 0
=> λx.x 0
=> 0
传递给自己而不是evil
,那么事情会变得有趣:
identity
我们最终得到了一个完全无意义的功能,但我们实现了一些重要的功能 - 我们创建了一个级别的递归。如果我们要评估(evil evil) 0
-> (λf.λy.(f y) λf.λy.(f y)) 0
=> λy.(λf.λy.(f y) y) 0
=> λf.λy.(f y) 0
=> λy.(0 y)
,我们会得到两个级别。使用(evil (evil evil))
,三个。所以我们需要做的不是将(evil (evil (evil evil)))
传递给它自己,我们需要传递一个函数,以某种方式为我们完成这个递归。特别是,它应该是具有某种自我应用的功能。我们想要的是Y-combinator:
evil
这个功能非常难以从定义中解决,所以最好只调用它λf.(λx.(f (x x)) λx.(f (x x)))
,看看当我们尝试用它来评估一些事情时会发生什么:
Y
正如我们所看到的,这种情况无穷无尽。我们所做的是Y evil
-> λf.(λx.(f (x x)) λx.(f (x x))) evil
=> λx.(evil (x x)) λx.(evil (x x))
=> evil (λx.(evil (x x))
λx.(evil (x x)))
=> evil (evil (λx.(evil (x x))
λx.(evil (x x))))
=> evil (evil (evil (λx.(evil (x x))
λx.(evil (x x)))))
,它接受第一个函数,然后接受一个参数并使用该函数计算该参数,并将其传递给{的特殊修改版本{1}}扩展以提供递归的函数。所以我们可以创建一个&#34;递归点&#34;通过减少evil
在evil
函数中。所以现在,只要我们看到一个使用递归的命名函数,就像这样:
evil
我们可以将其转换为:
evil (Y evil)
我们将函数转换为首先接受函数作为递归点的版本,然后我们提供函数λname(x).(.... some body containing (name arg) in it somewhere)
作为函数用作递归点。
这种作用的原因,以及让waaaaay回到作者的原始观点,是因为表达式λname-rec(f).λx.(...... body with (name arg) replaced with (f arg))
λname(x).((name-rec (Y name-rec)) x)
在开始自己的缩减之前不必完全减少Y name-rec
。我不能强调这一点。我们已经看到减少name-rec (Y name-rec)
导致无限循环,因此如果Y name-rec
函数中存在某种条件,递归就会起作用意味着Y name-rec
的下一步可能不需要减少。
这在许多编程语言中都会出现问题,包括功能语言,因为它们不支持这种懒惰的评估。此外,几乎所有编程语言都支持变异。也就是说,如果您定义变量name-rec
,稍后在同一代码中可以生成Y name-rec
,并且当它为3时引用x = 3
的所有旧代码现在都会看到{{1}这意味着你的程序可能会有完全不同的结果,如果旧代码是&#34;延迟&#34;具有惰性评估并且仅在稍后计算,因为到那时x = 5
可能是5.在一种语言中,任何时候都可以任意顺序任意执行事物,你必须完全消除你的程序依赖于语句顺序和时间变化值之类的东西。如果你不这样做,你的程序可以计算出任意不同的结果,具体取决于代码的运行顺序。
然而,编写没有任何秩序感的代码非常困难。我们看到了lambda演算有多复杂,只是试图让我们的头围绕琐碎的递归。因此,大多数函数式编程语言选择一个模型,系统地定义评估事物的顺序,并且它们永远不会偏离该模型。
Racket是Scheme的一种方言,它规定了在普通的Racket语言中,所有的表达式都会被评估并且急切地#34; (没有延迟)并且所有函数参数都是从左到右急切地评估,但是 Racket程序包含一些特殊的表单,可以让你有选择地使某些表达式变得懒惰,例如x
。 Haskell做了相反的事情,表达式默认进行延迟评估,让编译器运行一个&#34;严格分析器&#34;确定需要特别声明需要参数的函数需要哪些表达式才能得到热切评估。
正在制定的主要观点似乎是设计一种完全允许所有表达式单独懒惰或渴望的语言太不切实际,因为这会对您可以在语言中使用的工具造成限制很严重因此,记住功能语言为操作惰性表达式和渴望表达式提供的工具是很重要的,因为它们在所有实用的函数式编程语言中肯定不相同。