递归函数(七):不动点算子

递归函数(一):开篇
递归函数(二):编写递归函数的思路和技巧
递归函数(三):归纳原理
递归函数(四):全函数与计算的可终止性
递归函数(五):递归集与递归可枚举集
递归函数(六):最多有多少个程序
递归函数(七):不动点算子
递归函数(八):偏序结构
递归函数(九):最小不动点定理

    • -

回顾

以上几篇文章中,我们讨论了可计算性理论相关的一些内容,
可计算性与递归函数论存在着千丝万缕的联系,
不动点理论也是这样的,我们定义的递归函数一定存在吗?
在什么情况下它是存在的?

要回答以上这些问题,还要从方程,不动点,不动点算子说起。

约束方程

在中学时代,我们学过“方程”的概念,
方程可以简单表述为含有未知数的等式,例如,\(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
递归推断

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值