【问题标题】:Functional "simultanity"?功能“同时性”?
【发布时间】:2014-08-08 00:00:53
【问题描述】:

在这个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))

预排序评估表明我们必须在将 f(x) 插入 g() 之前将其完全展开。但这需要永远,这是..不方便。订单后评估表明我们可以做到这一点:

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 入门书,让我开始。

【问题讨论】:

  • 这个问题似乎是题外话,因为它属于 cs.stackexchange
  • 由于 lambda 演算没有副作用,您可以在任何您想要的地方开始扩展,所以他只保留了 f(x) 并将 g 首先扩展为 f(x) f(x)。然后他开始对它们进行两次扩展,以获得f 两次无限扩展的感觉。
  • 这是一大堆不连贯的(对我来说)漫无边际的说“函数式编程不是 lambda 演算”。嗯,呃。本文的其余部分主要依靠英语(和 Perl)的模棱两可的性质来“消除”其他“神话”。
  • 我同意,这是一篇非常糟糕的文章。如果“同时性”确实是 FP 中的一个东西,那么我们之前可能已经从更权威的来源听说过它。
  • 谁能用整个 λf(x) ::= x f(x) 和 λg(x) ::= x x 来解释他在说什么?

标签: functional-programming lambda-calculus


【解决方案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

在第一次缩减中,参数在被用于λx.x 函数之前被缩减。在第二个中,参数只是替换λx.x 函数体中——它在使用之前没有被缩减。在编程中使用这个概念时,它被称为“惰性求值”——在需要之前,您实际上不会求值(归约)表达式。需要注意的重要一点是,在 lambda 演算中,在替换之前参数是否被归约并不重要。 lambda 演算的数学证明只要两者都终止,无论哪种方式你都会得到相同的结果。这在编程语言中绝对不是这样,因为各种各样的事情(通常与程序状态的变化有关)都会使惰性求值与正常求值不同。

Lambda 演算需要一些扩展才能有用。没有办法命名事物。假设我们允许这样做。特别是,让我们为 lambda 演算中的函数创建自己的定义:

λname(arg).body

我们会说这意味着函数λarg.body绑定name,并且在任何随附的 lambda 表达式中的其他任何地方,我们都可以将name 替换为λarg.body。所以我们可以这样做:

λidentity(x).x

现在当我们写identity 时,我们只需将其替换为λx.x。然而,这引入了一个问题。如果命名函数引用自身会发生什么?

λevil(x).(evil x)

现在我们遇到了一个问题。根据我们的规则,我们应该能够将body 中的evil 替换为名称所绑定的内容。但是由于名称绑定到λx.(evil x),所以我们一试:

λevil(x).(evil x)
=> λevil(x).(λx.(evil x) x)
=> λevil(x).(λx.(λx.(evil x) x) x)
=> ...

我们得到一个无限循环。我们可以永远评估这个表达式,因为我们无法将它从我们特殊命名的 lambda 形式转换为常规 lambda 表达式。我们不能从具有我们特殊扩展的语言转到常规 lambda 演算,因为我们不能满足“用函数表达式替换 evil 绑定到”的规则。有一些技巧可以解决这个问题,但我们稍后会讲到。

这里很重要的一点是,这完全不同于常规的 lambda 演算程序,该程序无限计算并且永远不会完成。例如,考虑 self 应用函数,它接受一些东西并将其应用于自身:

λ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 减少为self self 一遍又一遍。这是在任何(图灵完备的)编程语言中都能找到的简单的旧无限循环。

这与我们的递归定义问题之间的区别在于我们的名称和定义不是 lambda 演算。它们是简写,我们可以通过遵循一些规则将其扩展为 lambda 演算。但在λevil(x).(evil x) 的情况下,我们无法将其扩展为 lambda 演算,因此我们甚至无法运行 lambda 演算表达式。我们命名的函数在某种意义上“编译失败”,类似于当您将编程语言 compiler 发送到无限循环中并且您的代码甚至永远不会启动,而不是实际的 runtime em> 循环。 (没错,就是entirely possible让编译器陷入死循环。)

有一些非常聪明的方法可以解决这个问题,其中之一就是臭名昭著的Y-combinator。基本思想是您将我们有问题的evil 函数更改为,而不是接受一个参数并尝试递归,接受一个参数并返回另一个接受参数的函数,所以你的@987654362 @ 表达式有两个参数可以使用:

λevil(f).λy.(f y)

如果我们评估evil identity,我们将得到一个新函数,它接受一个参数并使用它调用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))),三个。因此,我们需要做的不是将evil 传递给自身,而是需要传递一个以某种方式为我们完成此递归的函数。特别是,它应该是具有某种自我应用的功能。我们想要的是 Y-combinator:

λ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)))))

正如我们所见,这种情况会无限进行。我们所做的是采用evil,它接受第一个函数,then 接受一个参数并使用该函数评估该参数,并传递给它一个经过特殊修改的evil 函数版本扩展以提供递归。所以我们可以通过减少evil (Y evil)evil函数中创建一个“递归点”。所以现在,每当我们看到一个使用递归的命名函数时:

λname(x).(.... some body containing (name arg) in it somewhere)

我们可以将其转换为:

λname-rec(f).λx.(...... body with (name arg) replaced with (f arg))
λname(x).((name-rec (Y name-rec)) x)

我们把函数变成一个版本,首先接受一个函数作为递归点,然后我们提供函数Y name-rec作为递归点。

之所以可行,并且让 waaaaay 回到作者的原点,是因为表达式 name-rec (Y name-rec) 在开始自己的归约之前不必完全归约 Y name-rec。我不能强调这一点。我们已经看到减少Y name-rec 会导致无限循环,因此如果name-rec 函数中存在某种条件,则递归有效,这意味着Y name-rec 的下一步可能不需要减少。

这在许多编程语言中都存在问题,包括函数式语言,因为它们不支持这种惰性求值。此外,几乎所有编程语言都支持mutation。也就是说,如果你定义了一个变量x = 3,那么稍后在相同的代码中你可以创建x = 5,并且所有引用x的旧代码现在都将x视为5。这意味着如果旧代码通过惰性评估“延迟”并且仅在以后计算,您的程序可能会产生完全不同的结果,因为到那时 x 可能是 5。在一种可以随时以任何顺序任意执行事物的语言中,您必须完全消除程序对语句顺序和时变值等事物的依赖。如果你不这样做,你的程序可能会根据你的代码运行的顺序计算出任意不同的结果。

然而,编写毫无秩序感的代码是极其困难的。我们看到了如何复杂的 lambda 演算只是试图让我们的头脑了解微不足道的递归。因此,大多数函数式编程语言都会选择一个系统地定义评估顺序的模型,并且它们永远不会偏离该模型。

Racket,Scheme 的一种方言,规定在正常的 Racket 语言中,所有表达式都“急切”地求值(没有延迟),所有函数参数都急切地从左到右求值,但是 Racket 程序包括特殊形式,可让您选择性地使某些表达式变得惰性,例如 (promise ...)。 Haskell 则相反,表达式默认 为惰性求值,并让编译器运行“严格性分析器”来确定特别声明需要急切求值的参数的函数需要哪些表达式。 p>

主要观点似乎是,设计一种完全允许所有表达式单独惰性或急切的语言太不切实际了,因为这对您可以在该语言中使用的工具造成了严重的限制。因此,重要的是要记住函数式语言为您提供了哪些工具来操作惰性表达式和急切表达式,因为它们在所有实用的函数式编程语言中肯定不是等价的。

【讨论】:

  • 我不确定你在说λx.(x x) 之类的东西实际上不是 lambda 演算并且它是它的简写(除了几个缺少括号)的部分中的意思。它遵循 lambda 演算的所有规则(您提供的表达式的 β 归约也是如此),并且它们的格式正确(或者它们会带有更多的括号)。没有“编译” lambda 表达式的概念,只有 λ-abstraction、α-conversion、η-conversion 和 β-reduction,它们对应于(untyped)lambda 演算表达式的求值。
  • 我不认为我说过 λx.(x x) 不是 lambda 演算,我想说的是函数的 named 版本的简写是不是 lambda 演算。这是一个可以被等效的 lambda 演算表达式替换的扩展(因此是“编译”类比),但在某些情况下,这个替换规则会导致无限循环。使用扩展的天真的递归尝试evil 就是这种情况。 λx.(x x) 是 lambda 演算,λself(x).(x x) 不是,它是一种简写形式,必须首先转换为等效的 lambda 演算。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-05
  • 1970-01-01
  • 2018-11-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多