fix 函数

fix 是一个在 Data.Function 模块中定义的函数,它是对于递归的封装,可以用于定义不动点函数。

fix :: (a -> a) -> a
fix f = let x = f x in x

fix 函数的定义使用了递归绑定,比较难以理解:

fix f
= let x = f x in x
= let x = f x in f x
= let x = f x in f (f x)
= let x = f x in f (f (f x))
= let x = f x in f (f .. (f (f x)) ..)
= let x = f x in f . f . ... . f . f $ x

即 fix 函数的实质是无限多次调用函数 f,直至函数 f 的返回值不依赖于参数时递归调用终止。

Prelude Data.Function> fix (const "hello")
"hello"
Prelude Data.Function> fix (1:)
[1, 1, ...
fix (const "hello")
= let x = const "hello" x in x
= let x = "hello" in x
= "hello" fix (1:)
= let x = 1 : x in x
= let x = 1 : x in 1 : x
= let x = 1 : x in 1 : 1 : x
= let x = 1 : x in 1 : 1 : ... 1 : x
= [1, 1, ...]

fix 函数与递归

借助于 fix 函数我们可以将递归函数改写为非递归函数。

以下是计算阶乘的函数的递归版本和使用 fix 函数的非递归版本。

Prelude Data.Function> factorial n = if n == 0 then 1 else n * factorial (n-1)
Prelude Data.Function> factorial 3
6
Prelude Data.Function> factorial' = fix (\rec n -> if n == 0 then 1 else n * rec (n-1))
Prelude Data.Function> factorial' 3
6

这里 fix (\rec n -> if n == 0 then 1 else n * rec (n-1)) 就是非递归版本,下面在解释器里检查它的类型

Prelude Data.Function> :t (\rec n -> if n == 0 then 1 else n * rec (n-1))
(\rec n -> if n == 0 then 1 else n * rec (n-1))
:: (Eq p, Num p) => (p -> p) -> p -> p
Prelude Data.Function> :t fix (\rec n -> if n == 0 then 1 else n * rec (n-1))
fix (\rec n -> if n == 0 then 1 else n * rec (n-1))
:: (Eq p, Num p) => p -> p

类型分析:

  • (\rec n -> ...) 的类型为 (p -> p) -> p -> p

    参数 rec 的类型为 (p -> p),参数 n 的类型为 p

    (Eq p, Num p) 说明类型 p 是可以比较的数值类型。
  • 非递归版本 fix (\rec n -> ...) 的类型为 p -> p
  • 即非递归版本中 fix 的类型为((p -> p) -> p -> p) -> p -> p
  • 对比 fix 函数的定义fix :: (a -> a) -> a,可知原先定义中的类型 a 被替换成了 (p -> p)

手动计算:

fix (\rec n -> if n == 0 then 1 else n * rec (n-1)) 3
= (let x = (\rec n -> if n == 0 then 1 else n * rec (n-1)) x in x) 3
= let x = (\rec n -> if n == 0 then 1 else n * rec (n-1)) x in x 3
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in x 3
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in (\n -> if n == 0 then 1 else n * x (n-1)) 3
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * (x 2)
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * ((\n -> if n == 0 then 1 else n * x (n-1)) 2)
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * (2 * (x 1))
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * (2 * ((\n -> if n == 0 then 1 else n * x (n-1)) 1))
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * (2 * (1 * (x 0)))
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * (2 * (1 * ((\n -> if n == 0 then 1 else n * x (n-1)) 0)))
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 3 * (2 * (1 * 1))
= let x = (\n -> if n == 0 then 1 else n * x (n-1)) in 6
= 6

通过手工计算,可以看出 rec 实质上是由 fix 函数所传入的计算阶乘的函数 factorial。

rec 的类型就是递归版本 factorial 的类型。

事实上,递归版本和使用 fix 函数的非递归版本是非常相似的。

-- 递归版本 1
factorial 0 = 1
factorial n = n * factorial (n-1)
-- 递归版本 2
factorial n = if n == 0 then 1 else n * factorial (n-1)
-- 递归版本 3
factorial = \n -> if n == 0 then 1 else n * factorial (n-1)
-- 非递归版本 1
factorial' = fix (\rec n -> if n == 0 then 1 else n * rec (n-1))
-- 非递归版本 2
factorial' = fix factorial_ where
factorial_ rec 0 = 1
factorial_ rec n = n * rec (n-1)

两相对比,不难发现只需要履行一定的步骤就可将递归版本转化为使用 fix 函数的非递归版本。

使用 lambda 的改写方法:

  1. 如果递归版本 factorial 的函数定义分段进行,我们需要使用 if else 语句或者 case of 语句将所有定义式合成为一个。
  2. 将递归版本 factorial 的全部参数(这里只有 n)都移到函数定义式的右边,也就是将函数定义改写为一个lambda。
  3. 在这个 lambda 的所有参数(这里只有 n)之前添加一个 rec 参数,它的类型应该和递归版本 factorial 函数的类型相同。
  4. 将这个 lambda 中所有对于递归版本 factorial 函数的调用改为对 rec 的调用。
  5. 将这个改写完毕的 lambda 作为参数传给 fix 即可生成非递归版本 factorial'。

使用具名函数的改写方法:

  1. 将递归版本的函数名 factorial 按照一定规则(比如加下划线)改名为 factorial_。
  2. 在这个函数的所有参数(这里只有 n)之前添加一个 rec 参数,它的类型应该和递归版本 factorial 函数的类型相同。
  3. 将这个函数中所有对于递归版本 factorial 函数的调用改为对 rec 的调用。
  4. 将这个改写完毕的函数 factorial_ 作为参数传给 fix 即可生成非递归版本 factorial'。
  5. 如果需要将函数 factorial_ 合并进入非递归版本,可以使用 where 子句将函数 factorial_ 改为局部函数。

使用 fix 改写递归函数的例子

map 函数

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs

改写过程1(使用 lambda):

首先合成所有定义,得到
map f list =
case list of
[] -> []
(x:xs) -> f x : map f xs
将参数 f 和 list 移到右边,得到
map = \f list ->
case list of
[] -> []
(x:xs) -> f x : map f xs
添加 rec 参数,替换 map 可得到
\rec f list ->
case list of
[] -> []
(x:xs) -> f x : rec f xs
将 lambda 传给 fix,可得到
map2 = fix $ \rec f list ->
case list of
[] -> []
(x:xs) -> f x : rec f xs

改写过程2(使用具名函数):

改名为 map_
添加 rec 参数,替换 map 可得到
map_ rec _ [] = []
map_ rec f (x:xs) = f x : rec f xs
将 map_ 传给 fix,可得到
map3 = fix map_
使用局部函数的话,可以将两者合并
map3 = fix map_ where
map_ rec _ [] = []
map_ rec f (x:xs) = f x : rec f xs

改写过程3(使用 lambda):

首先合成所有定义,得到
map f list =
case list of
[] -> []
(x:xs) -> f x : map f xs
将map f 看成一个函数,只将参数 list 移到右边,得到
map f = \list ->
case list of
[] -> []
(x:xs) -> f x : map f xs
添加 rec 参数,替换 map f 可得到
\rec list ->
case list of
[] -> []
(x:xs) -> f x : rec xs
将 lambda 传给 fix,可得到
map4 f = fix $ \rec list ->
case list of
[] -> []
(x:xs) -> f x : rec xs

fix 函数与不动点

f (fix f)
= f (let x = f x in x)
= let x = f x in f x
= let x = f x in f . f . ... . f . f $ x
= fix f 所以 fix 函数也可以定义为
fix f = f (fix f)

即 fix 函数的意义在于寻找函数 f 的不动点 y = fix f,使得 f y == y。

一阶函数的不动点是个常数,二阶以上的高阶函数的不动点是低一阶的函数。

在 fix 函数的定义中,等式右边只有对参数的引用,所以 fix 函数是一个组合子(combinator)。这也是它被定义在Data.Function 这个组合子专用模块的原因。

在计算机科学中,fix 函数这个用来求函数 f 的不动点的组合子被称为不动点组合子或 Y 组合子。

下面计算 y,使得 cos y == y。

递归版本cosFixpointExplicit

cosFixpointExplicit x =
if cos x == x
then x
else cosFixpointExplicit (cos x)

经过改写可得到使用 fix 函数的非递归版本

cosFixpoint x =
fix (\f b ->
if cos b == b
then b
else f (cos b)
) x
或者
cosFixpoint2 x =
($ x) . fix $ \f b ->
if cos b == b
then b
else f (cos b)
或者
cosFixpoint3 x =
flip fix x $ \f b ->
if cos b == b
then b
else f (cos b)
*Main> cosFixpoint 3
0.7390851332151607
*Main> cosFixpoint 4
0.7390851332151607
*Main> cos it
0.7390851332151607
*Main> cos it == it
True

即当 y == 0.7390851332151607 时,cos y == y。

不动点与递归

fix 函数将不动点和递归这两者结合了起来。

下面证明上述由递归版本转向使用 fix 函数的非递归版本的改写过程是有效的。

factorial = \n -> if n == 0 then 1 else n * factorial (n-1)
等价于
factorial = (\rec n -> if n == 0 then 1 else n * rec (n-1)) factorial
也就是说 factorial 是 (\rec n -> if n == 0 then 1 else n * rec (n-1)) 这个函数的不动点。
对比 fix 函数的定义
fix f = f (fix f)
令 f = (\rec n -> if n == 0 then 1 else n * rec (n-1)) 可得以下等式:
factorial = fix (\rec n -> if n == 0 then 1 else n * rec (n-1))
于是
factorial = \n -> if n == 0 then 1 else n * factorial (n-1)
等价于
factorial = fix (\rec n -> if n == 0 then 1 else n * rec (n-1))

由递归版本转向使用 fix 函数的非递归版本的改写过程的实质是:

  • 通过给递归版本 factorial 函数添加参数 rec 形成高一阶的非递归函数 factorial_。
  • 参数 rec 实质上就是 factorial 函数自身,所以参数 rec 与 factorial 函数类型相同。
  • 递归函数 factorial 是高一阶的非递归函数 factorial_ 的不动点。
  • 将非递归函数 factorial_ 作为参数传递给 fix 函数形成非递归版本 factorial'。
  • 在非递归版本中 factorial' 由 fix 函数带动非递归函数 factorial_ 不断进行递归求解。

手动计算(采用 fix函数的第二个定义fix f = f (fix f)):

factorial = \n -> if n == 0 then 1 else n * factorial (n-1)
factorial_ = (\rec n -> if n == 0 then 1 else n * rec (n-1))
factorial = fix factorial_ factorial 3
= fix factorial_ 3
= factorial_ (fix factorial_) 3
= (\rec n -> if n == 0 then 1 else n * rec (n-1)) (fix factorial_) 3
= if 3 == 0 then 1 else 3 * fix factorial_ 2
= 3 * fix factorial_ 2
= 3 * factorial_ (fix factorial_) 2
= 3 * (\rec n -> if n == 0 then 1 else n * rec (n-1)) (fix factorial_) 2
= 3 * (if 2 == 0 then 1 else 2 * fix factorial_ 1)
= 3 * (2 * fix factorial_ 1)
= 3 * (2 * factorial_ (fix factorial_) 1)
= 3 * (2 * (\rec n -> if n == 0 then 1 else n * rec (n-1)) (fix factorial_) 1)
= 3 * (2 * (if 1 == 0 then 1 else 1 * fix factorial_ 0))
= 3 * (2 * (1 * fix factorial_ 0))
= 3 * (2 * (1 * factorial_ (fix factorial_) 0))
= 3 * (2 * (1 * (\rec n -> if n == 0 then 1 else n * rec (n-1)) (fix factorial_) 0))
= 3 * (2 * (1 * (if 0 == 0 then 1 else 0 * fix factorial_ -1)))
= 3 * (2 * (1 * 1))
= 6

参考链接

Haskell/Fix and recursion

How do I use fix, and how does it work?

Grokking Fix

Haskell语言学习笔记(78)fix的更多相关文章

  1. Haskell语言学习笔记(88)语言扩展(1)

    ExistentialQuantification {-# LANGUAGE ExistentialQuantification #-} 存在类型专用的语言扩展 Haskell语言学习笔记(73)Ex ...

  2. Haskell语言学习笔记(79)lambda演算

    lambda演算 根据维基百科,lambda演算(英语:lambda calculus,λ-calculus)是一套从数学逻辑中发展,以变量绑定和替换的规则,来研究函数如何抽象化定义.函数如何被应用以 ...

  3. Haskell语言学习笔记(69)Yesod

    Yesod Yesod 是一个使用 Haskell 语言的 Web 框架. 安装 Yesod 首先更新 Haskell Platform 到最新版 (Yesod 依赖的库非常多,版本不一致的话很容易安 ...

  4. Haskell语言学习笔记(20)IORef, STRef

    IORef 一个在IO monad中使用变量的类型. 函数 参数 功能 newIORef 值 新建带初值的引用 readIORef 引用 读取引用的值 writeIORef 引用和值 设置引用的值 m ...

  5. Haskell语言学习笔记(39)Category

    Category class Category cat where id :: cat a a (.) :: cat b c -> cat a b -> cat a c instance ...

  6. Haskell语言学习笔记(72)Free Monad

    安装 free 包 $ cabal install free Installed free-5.0.2 Free Monad data Free f a = Pure a | Free (f (Fre ...

  7. Haskell语言学习笔记(44)Lens(2)

    自定义 Lens 和 Isos -- Some of the examples in this chapter require a few GHC extensions: -- TemplateHas ...

  8. Haskell语言学习笔记(38)Lens(1)

    Lens Lens是一个接近语言级别的库,使用它可以方便的读取,设置,修改一个大的数据结构中某一部分的值. view, over, set Prelude> :m +Control.Lens P ...

  9. Haskell语言学习笔记(92)HXT

    HXT The Haskell XML Toolbox (hxt) 是一个解析 XML 的库. $ cabal install hxt Installed hxt-9.3.1.16 Prelude&g ...

随机推荐

  1. 不同版本Eclipse对JDK版本要求

    原文:https://blog.csdn.net/kevin_pso/article/details/54971739 1.Eclipse 4.6 (Neon)---需要JDK1.8版本,官网解释如下 ...

  2. Webbrowser指定IE内核版本(更改注册表)

    如果电脑上安装了IE8或者之后版本的IE浏览器,Webbrowser控件会使用IE7兼容模式来显示网页内容.解决方法是在注册表中为你的进程指定引用IE的版本号. 比如我的程序叫做a.exe 对于32位 ...

  3. for练习.html

    <script> 偶数 var str=""; for (var i = 1 ; i <= 100; i++){ if (i%2 == 0) { //str = ...

  4. Java - 26 Java 数据结构

    Java 数据结构 Java工具包提供了强大的数据结构.在Java中的数据结构主要包括以下几种接口和类: 枚举(Enumeration) 位集合(BitSet) 向量(Vector) 栈(Stack) ...

  5. Intorduction To Computer Vision

    本文将主要介绍图像分类问题,即给定一张图片,我们来给这张图片打一个标签,标签来自于预先设定的集合,比如{people,cat,dog...}等,这是CV的核心问题,图像分类在实际应用中也有许多变形,而 ...

  6. CS229 6.11 Neurons Networks implements of self-taught learning

    在machine learning领域,更多的数据往往强于更优秀的算法,然而现实中的情况是一般人无法获取大量的已标注数据,这时候可以通过无监督方法获取大量的未标注数据,自学习( self-taught ...

  7. CS229 6.7 Neurons Networks whitening

    PCA的过程结束后,还有一个与之相关的预处理步骤,白化(whitening) 对于输入数据之间有很强的相关性,所以用于训练数据是有很大冗余的,白化的作用就是降低输入数据的冗余,通过白化可以达到(1)降 ...

  8. redis如何随系统启动

    Redis可以通过命令redis-server启动,但这种启动方式适用于开发环境,对于生产环境来说,配置好redis的配置文件,并使redis随linux启动则更加方便些,下面则记录下redis如何随 ...

  9. jmeter分布式压力测试实践+登录为例

    1.一张分布式压力的图解,如下 准备: 1.两台slave 2.一个master 3.待测目标地址 http://XXX 准备环境:linux环境,master如果可以最好有可视化电脑界面,便于jmx ...

  10. 安全测试7_Web安全在线工具

    1.搜索引擎语法简单讲解:(实际上就是搜索引擎的高级搜索) 类似百度:可以看到下图我们是想在指定站点搜索包含login的页面,搜索语法为site:(testphp.vulnweb.com) " ...