后面的正文中详细描述了Z/Y组合子的python实现过程,前面的方法总结则是将正文中的各种方法提取出来,方便日后查看。
文章目录
总结正文中的Z组合子在python中实现阶乘函数的不同方法
法1: 未库里化的形式
分开写如下:
fact_base = (lambda rec, x:
1 if x == 0 else rec(rec, x - 1) * x)
fact = lambda x: fact_base(fact_base, x)
fact(3)
合并写如下:
factorial = lambda N: (lambda rec, N: 1 if N == 1 else rec(rec, N - 1) * N)(lambda rec, N: 1 if N == 1 else rec(rec, N - 1) * N, N)
factorial(3)
提取组合子写法如下, 注意此处其实已经部分库里化了, 否则无法得到单参数的fact函数:
mkrec = lambda f: lambda x: f(f, x)
fact = mkrec(lambda rec, N: 1 if N == 1 else rec(rec, N - 1) * N)
fact(3)
法2: 库里化后的形式
分开写如下:
fact_base = (lambda rec: lambda x:
1 if x == 0 else rec(rec)(x - 1) * x)
fact = lambda x: fact_base(fact_base)(x)
fact(3)
合并写如下:
factorial = lambda N: (lambda rec: lambda N: 1 if N == 1 else rec(rec)(N - 1) * N)(lambda rec: lambda N: 1 if N == 1 else rec(rec)(N - 1) * N)(N)
factorial(3)
提取组合子写法如下:
mkrec = lambda f: lambda x: f(f)(x)
fact = mkrec(lambda rec: lambda N: 1 if N == 1 else rec(rec)(N - 1) * N)
fact(3)
提取组合子得到
改进版本1:
注意此式进一步将上面提取的组合子进行了简化, 我们只用在fact中实现传入更多的参数即可:
mkrec = lambda f: f(f)
fact = mkrec(lambda rec: lambda x:
1 if x == 0 else rec(rec)(x - 1) * x)
fact(3)
改进版本2:
注意在下式中, 不用再将rec写作库里化的形式, 我们将库里化的实现, 隐藏在了mkrec_niac中:
mkrec = lambda f: f(f)
mkrec_nice = (lambda g:
mkrec(lambda rec:
g(lambda y: rec(rec)(y))))
fact = mkrec_nice(lambda rec: lambda x:
1 if x == 0 else rec(x - 1) * x)
改进版本3:
mkrec_nice = (lambda g:
( lambda rec: g(lambda y: rec(rec)(y)) )
( lambda rec: g(lambda y: rec(rec)(y)) ))
fact = mkrec_nice(lambda rec: lambda x:
1 if x == 0 else rec(x - 1) * x)
fact(3)
Tree-Recursion中使用Z组合子
下面探索计算fibonacci数列时, 使用Z组合子的方法
其一般实现如下:
def fibo(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibo(n - 1) + fibo(n - 2)
fibo(10)
而其匿名函数使用组合子的实现为:
mkrec_nice = (lambda g:
( lambda rec: g(lambda y: rec(rec)(y)) )
( lambda rec: g(lambda y: rec(rec)(y)) ))
fibo = mkrec_nice(lambda f: lambda n:
0 if n == 0 else 1 if n == 1 else f(n - 1) + f(n - 2))
fibo(10)
其匿名函数的直接库里化实现为:
((lambda f: lambda n: 0 if n == 0 else
1 if n == 1 else f(f)(n - 1) + f(f)(n - 2))
(lambda f: lambda n: 0 if n == 0 else
1 if n == 1 else f(f)(n - 1) + f(f)(n - 2)))(10)
其匿名函数的非库里化实现为:
(lambda n:
(lambda f, n: 0 if n == 0 else 1 if n == 1 else f(f, n - 1) + f(f, n - 2))
(lambda f, n: 0 if n == 0 else 1 if n == 1 else f(f, n - 1) + f(f, n - 2), n))(10)
正文
在lambda函数的知识体系里, Y组合子非常重要, 因为用它可以定义出匿名的递归函数
其他文章都是直接给出难以理解的Y组合子的定义, 然后再来解释其原因
本文则正好相反, 先解释Y组合子的精髓, 然后再从中推导出Y组合子的定义
我们使用的语言, 是python语言, 这是一种eager evaluation(主动求值)的语言; 所以在python中, 是不能用正版的Y组合子的, 而其实用的是Y组合子的变种Z组合子; 只有在lazy evaluation(惰性计算)的语言中, 才有真正的Y组合子
Lazy evaluation is a method to evaluate a Haskell program. It means that expressions are not evaluated when they are bound to variables, but their evaluation is deferred until their results are needed by other computations. In consequence, arguments are not evaluated before they are passed to a function, but only when their values are actually used.
Technically, lazy evaluation means call-by-name plus Sharing. A kind of opposite is eager evaluation.
Lazy evaluation is part of operational semantics, i.e. how a Haskell program is evaluated. The counterpart in denotational semantics, i.e. what a Haskell program computes, is called Non-strict semantics. This semantics allows one to bypass undefined values (e.g. results of infinite loops) and in this way it also allows one to process formally infinite data.
While lazy evaluation has many advantages, its main drawback is that memory usage becomes hard to predict. The thing is that while two expressions, like
2+2 :: Int
and4 :: Int
, may denote the same value 4, they may have very different sizes and hence use different amounts of memory.An extreme example would be the infinite list
1 : 1 : 1 …
and the expressionlet x = 1:x in x
. The latter is represented as a cyclic graph, and takes only finite memory, but its denotation is the former infinite list.
Recursive definitions in Python
在python中, 一般场景下的递归函数, 是如下定义的:
def fact(x):
if x == 0: return 1
else: return fact(x - 1) * x
其计算过程也即:
fact(3) == fact(2) * 3 == fact(1) * 2 * 3 == fact(0) * 1 * 2 * 3 == 1 * 1 * 2 * 3 == 6
现在我们将其写成更贴近lambda表达式的形式:
def fact(x):
return 1 if x == 0 else fact(x - 1) * x
我们直接将递归写成python中的lambda实现形式:
fact = (lambda x: 1 if x == 0 else fact(x - 1) * x)
该命名后的lambda形式, 与前文中的def实现, 效果是相同的
print(fact(3)) # prints 6
Problem
上文中的lambda表达式, 并非是匿名的, 而且其在自指中使用了别名fact
所以说, 上式不能说是一个完全的lambda表达式, 而至少是受限制的, 也就是至少其名称是不能更换的
注意, 一段程序若有如下形式: x = value; ...x...
那么它总可以等价于一个lambda函数: (lambda x: ...x...)(value)
Recursion without direct self reference
我们的目标, 是写出递归函数, 但希望此函数是匿名的
但这怎么可能呢?
我们的切入点在, 无论如何, 一个函数要实现"递归", 那么它总得调用其自身, 也即它总是要自指的
其中的技巧, 就是要间接(indirect)自指, 也就是说, 虽然自指对于递归来说是必须的, 我们还可以不给它命名, 并且间接得实现自指
我们想想, 如果一个非递归函数 f
, 想要引用其自身, 那么我们可以很简单得, 直接将 f
自身作为参数传给它
比如说, 考虑下面的这个函数 f
, f会在调用函数g之前, 打印出"in f"语句的
而函数g则也传入了一个单参数(此参数可为函数或实值)的空lambda函数
def f(g):
print("in f")
g(lambda x: None)
调用了 f(lambda h: print("in g"))
之后, 就得到了如下的环境图:
我们如果直接将f自身作为参数传入f, 调用f(f)
后就实现了一种自调用, 得到了如下的环境图:
Making recursion happen
在如下结构中, 我们能够将参数g自身当作参数, 传给参数g自身, 并在外层包裹一层无参数lambda以避免无限循环的情况
def f(g):
print("in f")
return lambda: g(g)
我们调用f(f)
时, 返回无参数lambda, 然后再次调用此函数, 则会再次调用整个函数体, 并返回无参数lambda…无限循环, 每次都是以返回无参数lambda作为结束
上述的f(f)()()
等的调用, 与下面的显式(有名字)递归形式完全对应:
def ff():
print("in f")
return lambda: ff()
上面就是一个例子, 我们看到了一个非递归函数, 是如何表现出递归功能的
也就是令其中传入一个函数参数, 而在调用时, 让该参数等于该函数自身
That is the essence of the Y combinator!
上面所述, 就是Y/Z组合子的全部精髓了
我们直接用上面的方法, 来重新定义阶乘函数试试
The factorial function in lambda calculus
首先, 定义递归的基本结构fact_base
该lambda函数会传入两个参数: 其中一个是阶数x; 另一个是rec函数, 此函数在将来赋值时, 可将此lambda函数自身赋给它
fact_base = (lambda rec, x:
1 if x == 0 else rec(rec, x - 1) * x)
随后, 我们将fact_base函数自身传递给其自身, 从而"将此递归打上结"
将此过程抽象到如下 fact
函数中:
fact = lambda x: fact_base(fact_base, x)
如上述情况, 就已经得到了匿名的递归函数了
比如, 我们若想要计算 fact(3)
, 此调用, 即等价于 fact_base(fact_base, 3)
也即等价于 1 if 3 == 0 else fact_base(fact_base, 3 - 1) * 3
而此式中的 fact_base(fact_base, 3 - 1)
, 则可进一步计算为 1 if 2 == 0 else fact_base(fact_base, 2 - 1) * 2
经过类似上述步骤后, 最终会返回 6
A simplification: currying
currying = 库里化 = 单形参高阶化(Lecture5中讲到)
可以将接受多个参数的函数; 变成接受一个参数, 但返回另一个函数(此函数接收第二的参数)的函数
如下例, 就是使用了库里化, 从而将匿名递归函数极大简化的例子:
# assuming the curried:
fact_base = (lambda rec: lambda x:
1 if x == 0 else rec(rec)(x - 1) * x)
# we now just write:
fact = fact_base(fact_base)
上面用了库里化, 已经将匿名实现递归函数的过程, 极大简化了
但我们接下来还会进一步简化其形式
Abstracting the outer self application
以上的形式还是不够好, 我们必须要将上述的样板文件, 转换为好用的组合子函数
首先, 我们要定义一个函数, 此函数定义外层的, 自递归结构, 此函数可命名为 mkrec
, 因为此函数能够创造出递归
mkrec = lambda f: f(f)
fact = mkrec(lambda rec: lambda x:
1 if x == 0 else rec(rec)(x - 1) * x)
而现在fact结构, 就能够变得更加staightforward, 然而其结构还是十分复杂, 因为想要自递归, 还要使用 rec(rec)(x - 1)
Abstracting the inner self application
我们的目标是, 能够使用 rec(x - 1)
而非 rec
来调用自递归结构, 那么我们就要另外再写一种更漂亮形式的 mkrec_nice
mkrec_nice
所做的工作, 在其底层将 rec(rec)(x - 1)
改为 rec(x - 1)
, 如下列代码所示:
mkrec = lambda f: f(f)
mkrec_nice = (lambda g:
mkrec(lambda rec:
g(lambda y: rec(rec)(y))))
fact = mkrec_nice(lambda rec: lambda x:
1 if x == 0 else rec(x - 1) * x)
其环境图如下:
为了理解其工作方式, 我们来看看如下的代码:
fact = mkrec_nice(g)
其中的g为:
g = (lambda rec_nice: lambda x:
1 if x == 0 else rec_nice(x - 1) * x)
上面也就是我们需要写的真正的递归部分
将 mkrec_nice
写开来, 就能够得到如下形式:
fact = mkrec(lambda rec:
g(lambda y: rec(rec)(y)))
再将g给代入如上结构, 我们得到如下形式, 注意: 我们用 lambda y: rec(rec)(y)
替换了 rec_nice:
fact = mkrec(lambda rec:
(lambda x:
1 if x == 0 else (lambda y: rec(rec)(y))(x - 1) * x))
而进一步解开 lambda y: rec(rec)(y)
此lambda式后, 我们就得到了原本的我们已经理解过的形式:
fact = mkrec(lambda rec: lambda x:
1 if x == 0 else rec(rec)(x - 1) * x)
有了上面这个 mkrec_nice
组合子, 那么我们就能够用非常简单的boilerplate样板获得匿名的递归函数了
The Z combinator
mkrec_nice
其实是一种Z组合子, 我们现在进一步来理解一下, 什么是Z组合子
将 mkrec
函数解入 mkrec_nice
中, 得到如下形式:
mkrec_nice = (lambda g:
(lambda f: f(f))((lambda rec: g(lambda y: rec(rec)(y)))))
将 lambda f: f(f)
此结构解开, 得到如下形式:
mkrec_nice = (lambda g:
( lambda rec: g(lambda y: rec(rec)(y)) )
( lambda rec: g(lambda y: rec(rec)(y)) ))
而使用lambda表达式的形式, 将上述 mkrec_nice
组合子改写, 即得到:
Z = λg. (λr. g (λy. r r y)) (λr. g (λy. r r y))
其中的 λg.
表示 lambda g:
; r r y
表示 r(r)(y)
The real Y combinator
真正的Y组合子, 是比Z组合子更简单一点的, 因为在惰性计算(如Haskell, Lisp)语言中, 不止是函数能够递归, 普通的数值, 也能够递归
所以我们能够直接创建递归的值, 而不必处理函数调用和传参的问题
其实现如下:
mkrec_lazy = (lambda g:
mkrec(lambda rec:
g(rec(rec))))
解开上式后, 得到:
mkrec_lazy = (lambda g:
( lambda rec: g(rec(rec)) )
( lambda rec: g(rec(rec)) ))
一般的Y组合子(用惰性计算lazy evaluation语言中)的形式如下:
Y = λg. (λr. g (r r)) (λr. g (r r))
在python(主动求值eager evaluation语言)中则不能用此Y组合子, 只能用Z组合子, 因为Y组合子会导致无限循环
正文部分译自:
https://lptk.github.io/programming/2019/10/15/simple-essence-y-combinator.html