关于python中Y组合子的问题讨论
by Wenze Jin
在 The Structure and Interpretation of Computer Programs 这门课的hw03-Recursion中有如下的 JUST FOR FUN拓展问题,值得研究。
文章目录
Problem 7: Anonymous factorial
The recursive factorial function can be written as a single expression by using a conditional expression.
>>> fact = lambda n: 1 if n == 1 else mul(n, fact(sub(n, 1)))
>>> fact(5)
120
The ternary operator
<a> if <bool-exp> else <b>
evaluates to<a>
if<bool-exp>
is truthy and evaluates to<b>
if<bool-exp>
is false-y.
However, this implementation relies on the fact (no pun intended) that fact
has a name, to which we refer in the body of fact
. To write a recursive function, we have always given it a name using a def
or assignment statement so that we can refer to the function within its own body. In this question, your job is to define fact recursively without giving it a name!
Write an expression that computes n
factorial using only call expressions, conditional expressions, and lambda expressions (no assignment or def statements). Note in particular that you are not allowed to use make_anonymous_factorial
in your return expression. The sub
and mul
functions from the operator
module are the only built-in functions required to solve this problem:
from operator import sub, mul
def make_anonymous_factorial():
"""Return the value of an expression that computes factorial.
>>> make_anonymous_factorial()(5)
120
>>> from construct_check import check
>>> # ban any assignments or recursion
>>> check(HW_SOURCE_FILE, 'make_anonymous_factorial', ['Assign', 'AugAssign', 'FunctionDef', 'Recursion'])
True
"""
return 'YOUR_EXPRESSION_HERE'
问题分析
题设要求我们构建一个可以求n
的阶乘的递归函数。
一般实现
在我们的认识中,解决这样的问题通常可以使用这样的递归函数来解决:
def factorialR(n):
if n == 1:
return 1
else:
return n * factorialR(n-1)
同时我们又知道:
lambda表达式
lambda x: x + 1
可以创建一个匿名函数,这个函数需要一个参数x,实现功能是返回x + 1
例如
>>> f1 = lambda x: x + 1
>>> f1(2)
3
>>> (lambda x: x + 1)(2)
3
>>> (lambda f: lambda x: f(x + 1))(f1)(2)
4
>>> if_test = lambda x: 1 if x > 0 else 0 if x == 0 else -1
>>> [if_test(10), if_test(0), if_test(-10)]
[1, 0, -1]
lambda表达式的实现
可以如下用一个有名称的lambda表达式简洁的实现这个递归
>>> factlambda = lambda x: 1 if x == 1 else x * factlambda(x - 1)
>>> factlambda(5)
120
但是!!!
题目的要求是让我们输出一个匿名的lambda函数,也就是它不可以有名字,理论上不可以调用自己!同时题目为了防止我们偷鸡,会在代码中检查你有没有赋值,定义函数,调用自身函数以实现递归的功能。
有如下答案:
def make_anonymous_factorial():
return (lambda fact: lambda k: fact(fact,k))(lambda factcopy, k: 1 if k == 1 else k * factcopy(factcopy,k-1))
在这个答案中我们可以看到如下流程:
-
定义一个 lambda 它需要一个参数 fact,它的功能是返回一个需要一个参数k的函数,并且指定fact的参数数目为两个
-
在右侧的括号中,整个形成一个lambda function,作为左侧括号中的fact参数,这个函数接受两个参数
factcopy
,k
并对k进行判断 实现一个阶乘的功能 -
我们可以将右侧的函数理解为
def f(factcopy, k): if k == 1: return 1 else: return k * factcopy(factcopy, k - 1)
这样一个递归的框架,相比较普通的递归,我们发现参数多了一个
factcopy
这是因为,这是一个匿名函数!没有其他人帮你记住你叫什么名字,如果我们想要知道自己叫什么,我们必须写一个小纸条把自己的名字记住!现在还差一步:记自己名字的纸条准备好了,还差搞清楚自己叫什么!
-
在进行调用的过程中,如果我们为左侧的函数提供一个k,左侧则会调用右侧函数,变为
lambda k: f(f, k)
f 是我们根据上面的理解假设的一个名字,便于理解这个函数的结构,实际上匿名函数是没有名字的
现在,我们就能理解左侧括号内的功能了:
- 告诉右侧函数,你叫自己! 现在你可以用
自己(自己,k)
调用自己了! - 生成一个需要一个参数值
k
的函数
- 告诉右侧函数,你叫自己! 现在你可以用
最终解答
def make_anonymous_factorial():
return (lambda fact: lambda k: fact(fact,k))(lambda factcopy, k: 1 if k == 1 else k * factcopy(factcopy,k-1))
于是,这题到这就结束了,引出了下题的重要概念与思想
Problem 8: All-Ys Has Been
Given mystery function Y
, complete fib_maker
and number_of_six_maker
so that the given doctests work correctly.
When Y
is called on fib_maker
, it should return a function which takes a positive integer n
and returns the n
th Fibonacci number.
Similarly, when Y
is called on number_of_six_maker
it should return a function that takes a positive integer x
and returns the number of times the digit 6 appears in x
.
Hint: You may use the ternary operator
<a> if <bool-exp> else <b>
, which evaluates to<a>
if<bool-exp>
is truthy and evaluates to<b>
if<bool-exp>
is false-y.
Y = lambda f: (lambda x: x(x))(lambda x: f(lambda z: x(x)(z)))
fib_maker = lambda f: lambda r: 'YOUR_EXPRESSION_HERE'
number_of_six_maker = lambda f: lambda r: 'YOUR_EXPRESSION_HERE'
my_fib = Y(fib_maker)
my_number_of_six = Y(number_of_six_maker)
# This code sets up doctests for my_fib and my_number_of_six.
my_fib.__name__ = 'my_fib'
my_fib.__doc__="""Given n, returns the nth Fibonacci nuimber.
>>> my_fib(0)
0
>>> my_fib(1)
1
>>> my_fib(2)
1
>>> my_fib(3)
2
>>> my_fib(4)
3
>>> my_fib(5)
5
"""
my_number_of_six.__name__ = 'my_number_of_six'
my_number_of_six.__doc__="""Return the number of 6 in each digit of a positive integer n.
>>> my_number_of_six(666)
3
>>> my_number_of_six(123456)
1
"""
问题分析
在上题思想的基础上,本题定义了一个常量Y
其实Y就是我们所说的Y组合子
Y = lambda f: (lambda x: x(x))(lambda x: f(lambda z: x(x)(z)))
改个熟悉的形式
Y = lambda f: (lambda x: x(x))(lambda xcopy: f(lambda z: xcopy(xcopy)(z)))
Y组合子语义分析
Y
指向了一个lambda函数,这个函数的功能是获得一个f
,对其进行如下操作:
-
观察这个函数的内部,我们可以发现,函数的左侧括号内定义了一个函数,这个函数需要一个输入参数x
- 它的功能是返回一个东西,即将
x
作为x
的参数时生成的东西 - 同时它指定了
x
的参数数目为一个 - 它看起来非常像我们在上题当中说到的告诉右边那个笨笨的函数自己叫做***“自己”***
- 它的功能是返回一个东西,即将
-
接下来我们看一下右边那个笨笨的函数是什么,可以把它写成这样
def benben(xcopy, f): return f(lambda z: xcopy(xcopy, f)(z))
-
这个函数需要一个参数
xcopy
(不是刚才那个x) 以及从外面的lambda表达式里拿进来的f- 它的功能是,应用最外侧函数输入的函数
f
,并再次需要一个参数z
- 告诉
benben
自己叫做benben
也就是xcopy = benben
- 指定了最外侧输入的f函数参数为一个函数
- 嵌套应用了很多次f形成了一个对f的递归,你会发现,
benben
返回的应该还是一个函数,因为xcopy(xcopy, f)
后仍需要一个参数z - 这并不难理解,因为f的括号内还是一个
lambda
表达式,他需要一个参数z
- 到现在为止,我们对整个函数的运行方式有了粗略的理解,但仍不清楚具体的实现方式
- 它的功能是,应用最外侧函数输入的函数
Y组合子功能实现
现在,我们需要利用Y组合子的这些特性完成下面两个任务
1. 实现第n个Fibonacci数的计算
题面:
Y = lambda f: (lambda x: x(x))(lambda x: f(lambda z: x(x)(z)))
fib_maker = lambda f: lambda r: 'YOUR_EXPRESSION_HERE'
my_fib = Y(fib_maker)
我们需要完善这个fib_maker
解答
fib_maker = lambda f: lambda r: 0 if r == 0 else 1 if r == 1 else f(r - 1) + f(r - 2)
此时 fib_maker
就是一个函数, 它请求一个方法f 请求一个参数r
通过上面的分析, fib_maker
这个函数会被应用于f
,在benben
函数中,每次都会调用一次fib_maker
,每次调用的结果:
- 它的参数f是一个请求更高阶数参数的
fib_maker
- 它需要一个当前阶数的r参数
至此,我们发现,benben
教会了fib_maker
每次调用自己并请求一个参数哦~
2. 实现统计一个数字中有几个六
题面
Y = lambda f: (lambda x: x(x))(lambda x: f(lambda z: x(x)(z)))
number_of_six_maker = lambda f: lambda r: 'YOUR_EXPRESSION_HERE'
my_number_of_six = Y(number_of_six_maker)
我们需要完善这个my_number_of_six
这个在本次作业先前的Problem1
中有提到原递归式函数为:
def number_of_six(n):
"""Return the number of 6 in each digit of a positive integer n.
>>> number_of_six(666)
3
>>> number_of_six(123456)
1
"""
if n == 0:
return 0
else:
if n % 10 == 6:
return 1 + number_of_six(n // 10)
else:
return number_of_six(n // 10)
解答
number_of_six_maker = lambda f: lambda r: 0 if r == 0 else 1 + f(r // 10) if r % 10 == 6 else f(r // 10)
此时number_of_six_maker
是一个函数,它要求一个方法f
,一个参数r
和上面同样的,benben
函数使得number_of_six_maker
:
- 的f是请求一个参数的更高阶数
number_of_six_maker
- 它需要一个当前阶数的参数r
至此,benben
又教会了number_of_six_maker
自己叫什么,自己调用自己与自己吃饭饭哦~
至此,我们也学会了怎么去完成一个匿名递归函数哦***(运用Y组合子)***
最终解答
fib_maker = lambda f: lambda r: 0 if r == 0 else 1 if r == 1 else f(r - 1) + f(r - 2)
number_of_six_maker = lambda f: lambda r: 0 if r == 0 else 1 + f(r // 10) if r % 10 == 6 else f(r // 10)
我们从Y组合子中获得了什么
到此为止,我们通过Y组合子的方式,仅通过λλ演算的概念,实现了「递归」的概念。因此,可以论证出「递归」这个概念可以在函数式编程语言中是「派生」的,而不需要「原生」支持。我们仅仅需要λ演算就能模拟自然数(hw02-Higher-Order-Funtions-JFF)
、运算(hw02-Higher-Order-Funtions-JFF)
、递归这些概念了。
笔者因专业知识有限,如有疏漏,欢迎交流!
Email: wenzejin2004@gmail.com
金文泽 Wenze Jin
Nanjing University