递归函数(一):开篇
递归函数(二):编写递归函数的思路和技巧
递归函数(三):归纳原理
递归函数(四):全函数与计算的可终止性
递归函数(五):递归集与递归可枚举集
递归函数(六):最多有多少个程序
递归函数(七):不动点算子
递归函数(八):偏序结构
递归函数(九):最小不动点定理
-
- -
回顾
以上几篇文章中,我们讨论了可计算性理论相关的一些内容,
可计算性与递归函数论存在着千丝万缕的联系,
不动点理论也是这样的,我们定义的递归函数一定存在吗?
在什么情况下它是存在的?
要回答以上这些问题,还要从方程,不动点,不动点算子说起。
约束方程
在中学时代,我们学过“方程”的概念,
方程可以简单表述为含有未知数的等式,例如,\(3x+3=2\)。
未知数可以同时出现在等式的两边,例如,\(2x+3=2-x\)。
通过合并同类项,我们可以求解\(x\)。
在大学时代,我们还学过线性方程组和微分方程,
例如,求解矩阵的特征值和特征向量,\(Av=lambda v\)
二阶常微分方程(贝塞尔方程),\(x^2y''+xy'+(x^2-alpha ^2)y=0\)。
在计算机科学中,同样的未知“数”的思想,
还出现在了类型推导(例如:unification))与递归函数的定义中。
以上这些例子,方程是“约束”的一种表现形式。
我们回到最简单的阶乘函数fact
的定义式,
fact :: Int -> Int
fact 1 = 1
fact n = n * fact (n-1)
去掉语法糖,稍微修改一下,
fact :: Int -> Int
fact n = case n of
1 -> 1
n -> n * fact (n-1)
我们发现,fact
的定义和“方程”十分相似,fact
同时出现在了等式的两边,
阶乘函数,就是这个“方程”的“解”。
函数的不动点
在中学数学中,我们已经学过不动点了,只是当时印象不是那么深刻,
函数的不动点,是指被这个函数映射到其自身的那些点。
例如:\(f(x)=x^2-3x+4\),
则\(2\)是函数\(f\)的一个不动点,\(f(2)=2\)。
并不是每一个函数都有不动点,
例如,实数域上的函数\(f(x)=x+1\),就没有不动点,对于任意实数\(x\),永远都不等于\(x+1\)。
(不动点是和定义域有关的,以后我们还会重新讨论\(f(x)=x+1\)的不动点。
一般的,函数\(f(x)\)的不动点,指的是这样的\(x\),使得\(x=f(x)\)。
重新温习了不动点相关的知识之后,
我们就可以对上面的阶乘函数进行改造了,
我们要把阶乘函数看做另外一个函数的不动点。
定义函数\(g\),
g :: (Int -> Int) -> Int -> Int
g f n = case n of
1 -> 1
n -> n * f (n-1)
我们可以得到,g fact = fact
,
因此,fact
实际上就是函数g
的不动点。
于是,在“方程”中求解fact
的过程,
就转换成了求解函数g
的不动点的过程了。
不动点算子
我们怎样求解函数g
的不动点呢?
在Haskell中,可以很方便的定义一个高阶函数fix
,它可以用来求解任意函数的不动点,
fix :: (a -> a) -> a
fix g = let x = g x in x
我们试验一下fix
的强大威力,
fact 10
> 3628800
fix g 10
> 3628800
注意,fix g
得到了g
的不动点,即(fix g) = g (fix g)
。
有了fix
,我们就可以构造匿名递归函数了,
fact' :: Int -> Int
fact' = fix $ \fact -> \n -> case n of
1 -> 1
n -> n * fact (n-1)
fix
后面跟的函数没有名字,它是匿名的,但是经过fix
作用后,可以产生一个递归函数。
也就是说,为了实现递归,函数是可以没有名字的。
Y组合子
Y组合子,是Haskell B. Curry在研究\(lambda\)演算时发现的,
它的表现形式如下,
\(Y:=lambda f.(lambda x.(f (x x)) lambda x.(f (x x)))\)
在\(lambda\)演算中,(\(alpha\)转换和\(beta\)规约
我们可以证明,对于任何函数g
,\((Y g) = (g (Y g))\)。
因此,Y组合子是一个不动点算子,它可以得到任意函数的不动点。
其他的不动点组合子还有图灵不动点组合子\(Theta\),
\(Theta:=(lambda x.lambda y.(y (x x y))) (lambda x.lambda y.(y (x x y)))\)
讨论Y组合子在Haskell中的表示方式是有趣的,因为直接翻译过去会报类型错误,
y :: (a -> a) -> a
y = \f -> (\x -> f (x x)) (\x -> f (x x))
-- Occurs check: cannot construct the infinite type: r0 ~ r0 -> a
-- Expected type: r0 -> a
-- Actual type: (r0 -> a) -> a
-- In the first argument of ‘x’, namely ‘x’
-- In the first argument of ‘f’, namely ‘(x x)’
类型系统无法确定x
的类型。
问题出在表达式x x
上面,
假设x x
的类型为a
,则第一个x
的类型就应该为? -> a
,
于是,第二个x
的类型肯定也应该是? -> a
。(因为都是x
又因为x x
的类型为a
,
所以第一个x
的类型? -> a
中,?
的类型就应该是? -> a
,
(因为((? -> a) -> a)
作用到(? -> a)
才能得到a
?
的类型是? -> a
,因此?
应该是一个递归类型。
下面我们来定义递归类型Mu
,来帮助编译器进行恰当的类型推导,
newtype Mu a = Mu (Mu a -> a)
y :: (a -> a) -> a
y f = (\h -> h \\( Mu h) (\x -> f . (\(Mu g) -> g) x \\) x)
最后,fact'
就可以使用Y组合子来定义了。
fact' :: Int -> Int
fact' = y $ \fact -> \n -> case n of
1 -> 1
n -> n * fact (n-1)
总结
本文从简单的“方程”思想出发,引出了不动点的概念,
然后把递归函数看做了另外一个函数的不动点,
最后,我们讨论了Y组合子这样一个具体的不动点算子。
可是,这里隐藏着一个问题,我们看到fix
是可以求解任意函数的不动点的,
而对于以下递归函数succ
,即\(f(x)=x+1\),
succ :: Int -> Int
succ n = n+1
在实数域上是显然没有不动点的。
那么fix succ
是什么呢?
这个问题,我们将在后文中继续讨论。
参考
方程
特征值和特征向量
微分方程
不动点
不动点组合子
Haskell/Fix and recursion
Y Combinator in Haskell
素朴H-M与算法W
递归推断