Lambda 微积分如何添加数字?

How would the Lambda Calculus add numbers?

我一直在阅读 lambda 演算,喜欢它提出的想法,但有些事情我无法解释;

lambda 演算如何计算加法数?

我明白了

(\x . + x x) 3

3 + 3一样,也就是6,但是加法函数本来是怎么实现的呢?

它是 compilers/languages 必须内置的东西,还是只能由 lambda 演算定义?

假设您使用的是 Church numerals,而不是一些原始数字类型,加法是组合:

\x \y . (\z . x(y(z)))

如果您在 lambda 演算中添加了某种原始数值类型,则加法要么必须是原始数值类型,要么必须按照类似后继运算的方式进行定义。

是的,您可以在 lambda 演算中定义数字(实际上是任意数据类型)。这是想法。

首先,让我们选择要定义的数字。最简单的数字是 自然数 :0、1、2、3 等等。我们如何定义这些?通常的方法是使用 Peano axioms:

  1. 0是自然数。
  2. n为自然数,则Sn为自然数

这里,S表示nn+1的后继者。因此,前几个 Peano 自然数是 0、S0、SS0、SSS0 等等——这是一元表示。

现在,在lambda演算中,我们可以表示函数应用,所以我们可以表示Sn,但我们不知道如何表示0和S本身。但幸运的是,lambda 演算为我们提供了一种推迟选择的方法:我们可以将它们作为参数,让其他人来决定!让我们为给定的 0 写 z,为给定的 S 写 s。那么我们可以如下表示前几个数,写⟦n⟧ for "the lambda calculus representation of the natural number n":

  • ⟦0⟧ = λ z s . z
  • ⟦1⟧ = λ z s . s z
  • ⟦2⟧ = λ z s . s (s z)
  • ⟦3⟧ = λ z s . s (s (s z))

正如自然数nn对S的应用0一样,[=94的lambda-calculus表示=]n 是 n 副本 any 后继函数 s 的应用任意z。我们也可以定义继任者:

  • ⟦0⟧ = λ z s . z
  • ⟦S⟧ = λ n . λ z s . s (n z s)

在这里,我们看到后继者在确保n之后,将s的一个额外副本应用到n 使用相同的 zs。我们可以看到这给了我们相同的表示,使用 ⇝ for "evaluates to":

  • ⟦0⟧ = λ z s . z
  • ⟦1⟧ = ⟦S0⟧
    = (λ n . λ z s . s (n z s))(λ zs′ . z′)
    ⇝ λ z s . s ((λ zs′ . z′) z s)
    ⇝ λ z s . s z
  • ⟦2⟧ = ⟦SS0⟧
    = (λ n . λ z s . s (n z s))((λ n′ . λ zs′ . s′ (nzs′))(λ zs″ . z″))
    ⇝ (λ n . λ z s . s (n z s))(λ zs′ . s′ ((λ zs″ . z″) zs′))
    ⇝ (λ n . λ z s . s (n z s))(λ zs′ . sz′)
    ⇝ λ z s . s ((λ zs′ . sz′) z s)
    ⇝ λ z s . s (s z)
  • ⟦3⟧ = ⟦SSS0⟧
    = (λ n . λ z s . s (n z s))((λ n′ . λ zs′ . s′ (nzs′))((λ n″ . λ zs″ . s″ (nzs″))(λ zs‴ . z‴)))
    ⇝ (λ n . λ z s . s (n z s))((λ n′ . λ zs′ . s′ (nzs′))(λ zs″ . s″ ((λ zs‴ . z‴) zs″)))
    ⇝ (λ n . λ z s . s (n z s))((λ n′ . λ zs′ . s′ (nzs′))(λ zs″ . sz″))
    ⇝ (λ n . λ z s . s (n z s))(λ zs′ . s′ ((λ zs″ . sz″) zs′))
    ⇝ (λ n . λ z s . s (n z s))(λ zs′ . s′ (sz′))
    ⇝ λ z s . s ((λ zs′ . s′ (sz′)) z s)
    ⇝ λ z s . s (s (s z))

(是的,它变得密集且难以快速阅读。如果您觉得自己需要更多练习,那么完成它是一个很好的练习 – 它导致我在我最初写的内容中发现了一个错误!)

现在,我们已经定义了 0 和 S,这是一个好的开始,但我们还需要一个归纳原理。毕竟,这就是使自然数成为现实的原因!那么,这将如何运作?好吧,事实证明我们基本上已经准备好了。当以编程方式考虑我们的归纳原理时,我们需要一个函数,它将一个基本案例和一个归纳案例作为输入,并产生一个从自然数到某种输出的函数。我将调用输出 "a proof for n"。那么我们的输入应该是:

  1. 一个基本案例,这是我们对 0 的证明。
  2. 一个归纳案例,它是一个函数,它将 n 的证明作为输入并产生 Sn 的证明。

换句话说,我们需要某种零值和某种后继函数。但这些只是我们的 zs 参数!所以结果 我们将自然数表示为它们的归纳原理,我认为这很酷。

这意味着我们可以定义基本操作。我在这里只定义加法,剩下的留作练习。在我们的归纳公式中,我们可以定义加法如下:

  1. m + 0 = m
  2. m + Sn = S(m + n )

这是在第二个参数上归纳定义的。那么我们如何翻译呢?变成:

  • ⟦+⟧ = λ m n . λ z s . n (m z s) s

这是从哪里来的?好吧,我们将我们的归纳原理应用到 n。在基本情况下,我们 return m(使用环境 zs) , 同上。在归纳的情况下,我们将后继者(环境 s)应用于我们得到的结果。所以这一定是对的!

另一种看待这个问题的方式是,因为 n z s 适用 nsz,我们有 n (m z s) s 适用 nsm z s ,共 n + msz。同样,这是正确答案!

(如果您仍然不相信,我鼓励您设计一个像 ⟦1+2⟧ 这样的小例子;它应该小到易于处理但又大到至少有点有趣。)


现在我们了解了如何在纯无类型 lambda 演算中定义自然数的加法。如果您愿意,这里有一些额外的想法供您进一步阅读;这些更简洁,解释更少。

  1. 这种表示技术更普遍适用;它不仅适用于自然数。它被称为Church encoding, and can be adapted to represent arbitrary algebraic data types。正如我们用归纳原理表示自然数一样,我们用结构递归原理(它们的折叠)表示所有数据类型:数据类型的表示是一个函数,它为每个构造函数接受一个参数,然后应用 "constructor" 到所有必要的参数。所以:

    • 布尔值:
      • ⟦False⟧ = λ f t . f
      • ⟦True⟧ = λ f t . t
    • 元组:
      • ⟦(x,y)⟧ = λ p . p x y
    • 求和类型(data Either a b = Left a | Right b):
      • ⟦左 x⟧ = λ l rl x
      • ⟦右 y⟧ = λ l rr y
    • 列表(data List a = Nil | Cons a (List a)):
      • ⟦Nil⟧ = λ n cn
      • ⟦Cons x l⟧ = λ n cc x l

    请注意,在最后一种情况下,l 本身就是一个编码列表。

  2. 此技术也适用于类型设置,我们可以在其中讨论数据类型的折叠(或 变形)。 (我主要提到这个是因为我个人认为它真的很酷。)然后 data Nat = Z | S Natforall a. a -> (a -> a) -> a 同构,e 的列表与 forall a. e -> (e -> a -> a) -> a 同构,这只是common foldr :: (e -> a -> a) -> a -> [e] -> a 类型签名的一部分。全称量化的a表示自然数或列表本身的类型;它们需要被普遍量化,因此传递这些需要 higher-rank typesfoldr Cons Nil 是恒等函数这一事实证明了同构;对于编码为 n 的自然数,我们同样有 n Z S 恢复我们的原始列表。

  3. 如果您对我们只使用自然数这一事实感到困扰,我们可以定义整数的表示;例如,常见的 unary-style 表示是

     data Int = NonNeg Nat | NegSucc Nat
    

    这里,NonNeg n代表nNegSucc n代表-(n+1);否定情况下的额外 +1 确保存在唯一的 0。如果您愿意,可以直接说服自己,您可以使用具有真实数据类型的编程语言在 Int 上实现各种算术函数;然后可以通过 Church 编码将这些函数编码到无类型的 lambda 演算中,这样我们就设置好了。分数也可以成对表示,尽管我不知道确保所有分数都可以唯一表示的表示。表示实数变得棘手,但 IEEE 754 浮点数可以表示为 32、64 或 128 元组的布尔值,这是非常低效和庞大的,但可编码。

  4. 自然数(和整数等)的更有效表示也可用;例如,

     data Pos = One
              | Twice     Pos
              | TwiceSucc Pos
    

    编码正二进制数(Twice n2*n,或者在末尾加一个0TwiceSucc2*n + 1,或者在末尾加一个1 到最后;基本情况是 One,单个 1)。对自然数进行编码就像

    一样简单
     data Nat = Zero | PosNat Pos
    

    但随后我们的函数(例如加法)变得更复杂(但速度更快)。