前言

我们再来看看第1课中定义的toTfunc()函数。

def toTfunc(func):
    def res(tuple):
        return func(*tuple)
    
    return res


def multiply(a, b):
    return a * b

请注意,toTfunc()主体内的这个res()函数有点多余,我们希望定义更简洁。对于这个multiply()函数来说也是如此,尽管它很简单,但它需要所有这些句法结构。

第1部分 - λ表达式

为了避免显式编写def关键字,我们可以使用 匿名函数 语法。要以这种方式创建函数,我们使用lambda关键字,通过逗号枚举所有参数,然后在冒号符号后写下作为函数体的表达式。例如,我们可以像这样定义乘法和加法函数。

mul = lambda a, b: a * b
add = lambda a, b: a + b

或者我们可以像这样避免在toTfunc主体中创建冗余函数。

def toTfunc(func):
    return (lambda tuple: func(*tuple))

我们甚至可能两次使用这样的定义。这样我们就创建了一个函数,给定参数f,返回一个函数。

toTfunc = lambda f: (lambda t: f(*t))

此语法可用于通过显式指定一个或多个参数的值来减少函数采用的参数数量。

mul2 = lambda x: mul(x, 2)
add1 = lambda x: add(x, 1)

您还可以利用堆叠多个lambda来创建一个函数,该函数分别接受每个参数。也就是说,每次提供一个参数并返回一个函数,其主体中的未绑定参数实际上更少。

sep_mul = lambda a: lambda b: a * b
print(sep_mul(2)(3))
>>> 6

mul2 = sep_mul(2)
print(mul2(3))
>>> 6

我还想提一下,许多人发现lambda这个关键字太长了,它真的很快就会将屏幕弄得乱七八糟。可以在fn模块中找到替代语法。比如下边这个示例:

from fn import _


mul = _ * _
mul2 = _ * 2
square = _ ** 2
print(mul(2, 3), square(4))
>>> 6 16

基本上每个下划线对应一个单独的参数,文档中说这种语法类似于Scala[1]。 我[2]避免使用它,因为下划线与为我指定函数的冗余参数[3]密切相关,但我想你们中的一些人可能会发现使用这种符号编写会更愉快。我还建议您查看 GitHub 上的 fn.py 文档,它是一个旨在使 Python 中的函数式编程更加简单的模块。

现在,回顾一下nextSquare()函数。

from math import sqrt


def inc(x):
    return x + 1


def fnextSquare(x):
    return pow(inc(sqrt(x)), 2)

由于我们需要的函数很简单,我们可能会以函数方式编写它。无需实际定义所有函数,而是使用匿名函数[4]

lnextSquare = lambda x: (lambda y: y ** 2)((lambda z: z + 1)(sqrt(x)))
print(lnextSquare(25))
>>> 36.0

第2部分 - 组合

但我们也可以选择另一种方式来做到这一点。请注意,在这种情况下,在函数体中写入参数有点多余。我的意思是,我们仍根据其结果如何取决于其参数来定义函数。但是当我们定义这个nextSquare()函数时,它不是我们想要表达的依赖关系,我们想说要得到下一个平方,你先取平方根,然后增加,最后得到其平方。为了专注于函数的这个含义,我们可以使用 组合 。函数 f(x)g(x) 的组合是一个函数 h(x) ,使得给定一些参数,h(x) 返回将 g(x) 应用于这些参数的结果,然后将 f(x) 应用于结果。请注意,当我们说 h(x)=(fg)(x) 时,我们不需要明确指定它对给定参数做了什么,我们根据已有的函数完全定义了一个新函数。当我们以函数式的方式制作东西时,我们有很多预制的小函数和通用函数,可以用作更复杂函数的某种积木。因此,拥有用于这种组合方法的工具可能会非常有用。我们可以自己定义一个组合函数,但没有必要,因为 Python 的toolz模块中已经有一个了。

from toolz import compose


square = lambda x: x ** 2
inc = lambda x: x + 1
sqrt = lambda x: x ** 0.5
cnextSquare = compose(square, inc, sqrt)
print(cnextSquare(25))
>>> 36.0

使用这个compose函数,我们可以以一种非常直接的方式定义nextSquare。这里唯一需要注意的是,组合函数的顺序很重要,组合链中的最后一个函数将首先应用,依此类推。顺便说一下,这种定义函数而不将其参数作为参数写入函数体内的方法称为 无点样式 [5],它通常用于函数式编程。

第3部分 - 邱琦计数法

我想在这节课中展示的最后一件事,是用一个例子来说明匿名函数的机制与λ表达式的起源之间的联系。您可能听说过 图灵机 ——由阿兰·图灵[6]开发的一种抽象计算模型,该模型基于评估器在通过对其执行一些操作来处理某些数据存储[7]时改变其状态的想法。阿隆佐·邱琦[8]开发了另一种称为 λ演算 的模型。与图灵模型不同,在λ演算中,数据和操作之间没有严格的区别。也就是说,我们可以将数据表示为函数,并且可以像操作数据一样操作函数,因此数据和操作是同构的。该模型是被认为是函数式编程语言[9]的核心。其中一些概念使程序员能够做更多的事情,以至于许多语言都采用了其中的一部分。您已经知道在 Python 中将函数传递给其他函数以及从其他函数返回函数的能力,所以这个『操作函数就像操作数据一样』是很清楚的。那么,什么是『将数据表示为函数』呢?我想向你展示这个叫做 邱琦计数 的系统,它使用λ表达式来表示数字。

让我们将数字 n 定义为一个函数,它接受某个函数 f 并返回另一个函数,它实际上是这个 f 函数与其自身的 n 次组合。 也就是说,给定参数 x ,它返回应用于 xf ,然后将 f 应用于 f(x) ,依此类推 n 次。

zero = lambda f: lambda x: x
one = lambda f: lambda x: f(x)
two = lambda f: lambda x: f(f(x))
three = lambda f: lambda x: f(f(f(x)))

因此,零是一个函数,在给定任何函数 f 的情况下,它返回一个恒等函数,该函数只返回它的参数(即函数 f 被调用了0次)。等等!现在,我们如何得到给定数字 n 的下一个数?由于任何数字都是一个函数,它接受某个函数 f 并返回另一个由 ff 组成 n 次的函数。那么,给定这样的数字 n ,要返回下一个数 n+1 ,我们应该返回一个函数:给定某个函数 f , 将返回另一个函数,该函数是与 f 的组合,其本身调用了 n+1 次。 但这实际上是 fn 的组合。 因此,我们需要返回一个函数,给定 fx ,将返回与数字 n 相同的结果,但将 f 再应用于它一次。

succ = lambda n: lambda f: lambda x: f(n(f)(x))

为了检查它是否有效,我们可以用一个具体的 f 和一个具体的 x 来测试它,这将使它更清楚。最方便的例子是指定 x=0f(x)=x+1 [10]。首先,让我们测试我们的数字…

f = lambda x: x + 1
x = 0

zero(f)(x)
>>> 0

one(f)(x)
>>> 1

two(f)(x)
>>> 2

three(f)(x)
>>> 3

现在,让我们找出将 fx 应用于 3 的后继数字的结果。

succ(three)(f)(x)
>>> 4

它真的起作用了!我想再次指出,这些函数是『数字函数』,而不是应用某些特定 f 和某些特定 x 的结果,后者只是表示数字的其中一种方式。我们可能更喜欢一些其他的代表:

f = compose(chr, inc, ord)
x = 'A'

zero(f)(x)
>>> 'A'

one(f)(x)
>>> 'B'

two(f)(x)
>>> 'C'

three(f)(x)
>>> 'D'

succ(three)(f)(x)
>>> 'E'

现在,让我们想一些方法来对这些数字执行加法和乘法。加法应该是一个函数,给定数字 mn ,返回 数字 m+n ,这是一个函数,给定某个函数 f ,返回另一个函数,该函数是 f 与自身组合 m+n 次的结果。因此,给定一些函数 f 和一些参数 x ,数字 m+n 将返回 fx 的后续应用的结果,然后 ff(f(x)) ,依此类推 m+n 次。 但这与首先将函数 fx 上调用 n 次,然后将函数 f 调用 m 次的结果相同。因此,如果我们将 n(f)(x) 作为 m(f) 的参数,我们将得到我们想要的。让我们检查它是否真的像我们期望的那样工作。

plus = lambda m: lambda n: lambda f: lambda x: m(f)(n(f)(x))
print(plus(one)(two)(f)(x))
>>> 3

再一次,乘法函数应该取数字 mn 并返回一个数字 mn ,它是一个函数,这样给定某个其他函数 f ,它返回 f 与自身 mn 次组合的结果。请注意,如果我们给 m 数字不是 f 函数,而是 n(f) 函数,那么我们会得到 x 的函数。给定 x ,这个函数会 m 次将 n(f) 应用到 x 。但是『 m 次将 n(f) 应用到 x 』是什么意思?嗯,n(f)f 与自身组合 n 次的结果。将它应用于某个 x 一次,意味着连续 n 次将 f 应用于 x 。应用 n(f) 两次,意味着将连续 n 次把 f 应用到 x ,然后再应用 n 次到结果上——即连续调用 2n 次。因此,如果我们连续 m 次将 n(f) 应用于 x ,我们会恰好 mn 次把 f 应用于 x ,这正是我们想要的。让我们检查它是否真的有效:

times = lambda m: lambda n: lambda f: m(n(f))
print(times(two)(three)(f)(x))
print(times(three)(succ(three))(f)(x))
>>> 6
..| 12

在 Python 布尔值之外的、敌对的外星世界中,布尔值不是数字。因此,将布尔值表示为数字并不是一个令人满意的解决方案,因为我们已经实现了后者。这是布尔值作为函数的实现:

true = lambda x: lambda y: x
false = lambda x: lambda y: y

怎么理解?如果我们通过定义 x=1y=0 来表示每个函数布尔值,我们可以直观地理解它:

print(true(1)(0))
>>> 1

很好,但这一切都与能够对数据进行操作有关。我们如何实现常见的逻辑功能?

让我们创建一个『逻辑与』函数。其实它没那么复杂:采用两个布尔值 b1b2 ,如果第1个为真,那么结果应该就是函数中第2个参数的样子,否则结果已经被预先定义为假。然后逻辑与函数可以将第2个参数的调用结果返回,然后将逻辑与函数的第1个参数(按该顺序)返回到第1个参数本身。这样,如果 b1 的第1个参数为真,则 b1 将返回其第1个参数,即逻辑与函数的第2个参数,因此,将返回其第2个参数。如果 b1 为假,它将返回它的第2个参数,即 b1 本身,这是假的。以上是我们需要的行为。

另一个要制作的功能是『逻辑非』。好吧,如果该函数表示的值取决于它返回哪一个参数,则翻转调用提供的参数的顺序会改变该函数的布尔值。

und = lambda b1: lambda b2: b1(b2)(b1)
print('tt: {}, tf: {}, ft: {}, ff: {}'.format(
        und(true)(true)(1)(0),
        und(true)(false)(1)(0),
        und(false)(true)(1)(0),
        und(false)(false)(1)(0)
))
nut = lambda b: lambda x: lambda y: b(y)(x)
print(nut(true)(1)(0))

>>> tt: 1, tf: 0, ft: 0, ff: 0
..| 0

你可以用它做很多其他的事情,但这就是这一课的内容。如果你对它很好奇、认为它充满挑战,你可以尝试学习更多关于λ演算和组合逻辑在编程中的用法。这些是更高级、理论性更强的主题,但您可能会发现它们值得探索。

总结

在这一课中,我们介绍了λ表达式,并说明了如何在 Python 中进行定义,顺便还提了一下能让 Python 如 Scala 般顺滑的fn库;我们还讲解了函数之间的组合,提到了 Python 中的toolz库;我们最后讲解了邱琦计数法,比较了图灵机和λ演算的思维差异,还通过实际代码实现了基本的计数和数学运算,更以另外的方式定义了布尔值并实现逻辑运算。


  1. 嘿,我不就好这口嘛~ ↩︎

  2. 此处指原作者,但我(本人)觉得这种写法确实很香。 ↩︎

  3. 也就是所谓的通配符,但我(本人)有时也会将它当哑变量(定义了但从不使用)占位符。 ↩︎

  4. lambda x: (sqrt(x) + 1) ** 2的写法不好嘛?所有运算符都是返回新值的,根本不可能更改形参好吧? ↩︎

  5. 或者叫做 默认编程↩︎

  6. 英文原名为 Alan Turing↩︎

  7. 在大部分教材提及这件事时,示意图都会将这个『存储』画成一条『无限磁带』的样子。 ↩︎

  8. 英文原名为 Alonzo Church ,有些文章会写作『邱奇』。Who cares? ↩︎

  9. 比如 Lisp 与 Haskell 。前面提到的 Scala 虽然也有函数式特性存在,但它本身是多编程范式语言,玩不了邱琦计数这样的纯函数式『魔法』。 ↩︎

  10. 也就是『自增函数』。 ↩︎