最近看了赵姐夫的这篇博客http://blog.zhaojie.me/2009/08/recursive-lambda-expressions.html,主要讲的是如何使用 Lambda 编写递归函数。其中提到了不动点组合子这个东西,这个概念来自于函数式编程的世界,直接理解起来可能有些困难,所以我们可以一起来尝试使用 Lambda 来编写递归函数,以此来探索不动点组合子的奥秘。在阅读过程中,我们可以使用“C# 交互窗口”或者 Xamarin WorkBook 来运行给出的代码,因为 Lambda 表达式中的变量,类型大多会被省略掉,直接阅读起来可能有些难懂。

首先用常规手段写一个递归形式的阶乘

int facRec (int n)
{
return n == 1 ? 1 : n * facRec(n - 1);
}
facRec(5)
// 120

那么如何使用 Lambda 表示阶乘的递归形式呢?Lambda 是匿名函数,那么就不能直接在内部调用自己,不过函数的参数是可以有名字的,那么可以给这个 Lambda 添加一个函数参数,在调用的时候,就把这个 Lambda 自己作为参数传入,从而实现递归的效果:

delegate Func<int, int> F(F self);

F fac = (F f) => (int n) => n == 1 ? 1 : n * f(f)(n - 1);

fac(fac)(5)
// 120

您可能已经发现了,我没有把 F 定义为接受两个参数,第一个接受一个函数作为参数,第二个是要求阶乘的值,返回一个 int 结果的形式。这其实是一种函数式编程的做法——任何包含多个参数的函数都可以写成多个只包含一个参数的函数的组合的形式,我们把这种操作叫做“柯里化”,例如:

int sum(int a, int b, int c)
{
return a + b +c;
}
Func<int, Func<int ,int>> fSum(int a)
{
return (int b) =>
{
return (int c) =>
{
return a + b + c;
};
};
}
sum(1,2,3) == fSum(1)(2)(3)
//true

虽然fSum的返回值类型看起来有些鬼畜,但是完全是 C# 自己的原因——不能自动推断方法的返回值类型。

接着回到我们的探索过程,注意到第3行出现了f(f)这样的东西,那么可以把这种表达式提取出来,作为参数传入。

fac = (F f) => (int n) =>
{
Func<Func<int,int>, Func<int,int>> tmp = (Func<int,int> g) =>
{
return (int h) =>
{
if(h == 1)
return 1;
else
{
return h * g(h - 1);
}
};
};
return tmp(f(f))(n);
}; fac(fac)(5)
// 120

现在,可以看到第 5 行返回的函数看起来挺像我们最开始定义的普通形式的递归阶乘,何不尝试将其提取出来,然后在 fac 中调用。

Func<Func<int, int>, Func<int, int>> fac0 = (Func<int, int> g) =>
{
return (int h) =>
{
if(h == 1)
return 1;
else
{
return h * g(h - 1);
}
};
};
fac = (F f) => (int n) =>
{
return fac0(f(f))(n);
};
fac(fac)(5)
// 120

这下我们的 fac 函数就变得简短了很多,但是其中仍引用了一个在外部定义的函数,这让他变得不够“”,所以可以把这个函数作为参数传入

delegate F NewF(Func<Func<int, int>, Func<int, int>>  g);

NewF newFac = g =>
{
return (F f) => (int n) => g( f(f) )(n);
}; // 等价于
newFac = g => f => n => g(f(f))(n); newFac(fac0)(newFac(fac0))(5)

重复的东西又出现了,可以把newFac(fac0)提取出来,这样的话就需要一个接受 F 类型函数并返回一个 Func<int, int> 类型函数的东西——其实就是前面定义的 F 啦~

F sF = f => f(f);

sF(newFac(fac0))(5)
// 120

现在接着尝试把fac0从两层括号中解放出来,以实现柯里化。所以首先就需要定义一个接受跟newFac类型相同的委托作为参数,并返回一个委托,这个返回的委托接受一个参数,参数类型与 fac0 相同。

delegate Func<Func<Func<int, int>, Func<int, int>>, Func<int,int>> NewSF(NewF newF);

NewSF newSF = newF =>
{
return (Func<Func<int, int>, Func<int, int>> g) =>
{
var f = newF(g);
return f(f);
};
}; newSF(newFac)(fac0)(5)

newF 是一个 NewF 类型的委托,返回值的类型是 F。注意到 newFac = g => f => n => g(f(f))(n),这是一个纯函数,可以直接代入到newSF之中,所以上面的newSF可以进一步化简。首先用泛型化简 g 的类型,在泛型的特例化之后,g 的类型跟上面的 newSF 里面的 g 的类型其实是一样的。newSF的参数 newF 可以代换为 newFacnewFac(g) 的结果类型是 F ,也就是上面的 f,因为 f 需要把自身作为参数,所以就重新把 newFac(g) 作为参数传给 newFac(g) 返回的委托。

delegate T Y<T>(Func<T, T> g);

Y<Func<int, int>> y = g =>
{
return n =>
{
return newFac(g)(newFac(g))(n);
};
}; y(fac0)(5)
// 120

还记得我们得出 sF 的过程吗?接着把上面的 y 化简一下

y = g =>
{
return n =>
{
return sF(newFac(g))(n);
};
};
y(fac0)(5)
// 120

然后写的紧凑一些

y = g => n => sF(newFac(g))(n);

看看我们现在得到的成果:

sF = f => f(f);
newFac = g => f => n => g(f(f))(n);
y = g => n => sF(newFac(g))(n);
y(fac0)(5)

由于 C# 并不是一门函数式的语言,Lambda 表达式不能直接调用,必须要转换成委托类型才可以直接调用,所以导致了 y 函数依赖另外两个函数,不过由于依赖的两个函数都是纯函数,所以没啥影响。但是上面的式子仍可继续简化,下面我把 newFac 定义在 y 表达式的内部:

y = g =>
{
return n =>
{
NewF localNewFac = localG => f => localN => localG(f(f))(localN);
return sF(localNewFac(g))(n);
};
};
y(fac0)(5)

可以看到 localNewFac 接受一个 localG 作为参数,然后返回一个 lambda 表达式,然后在第6行把 g 作为了实参传递给 localNewFac,这么看来,localNewFac 其实没必要接受一个 localG 作为参数,只要在闭包中捕获外部的变量 g 就好了

y = g =>
{
return n =>
{
F localF = f => localN => g(f(f))(localN);
return sF(localF)(n);
};
};
y(fac0)(5)

由于有 sF 的存在,编译器就有能力推断 sF 的参数类型,上面的代码就可以简化为:

y = g =>
{
return n =>
{
return sF(f => localN => g(f(f))(localN))(n);
};
};
y(fac0)(5)

现在,我们就可以得到下面两个式子:

sF = f => f(f);
y = g => n => sF (f => m => g(f(f)) (m)) (n);
y(fac0)(5)
// 120

现在来重新审视一下 fac0 的类型,可以将其定义为下面的样子

delegate T FT<T>(T f);

FT<Func<int, int>> newFac0 = (Func<int, int> f) => n => n == 1 ? 1 : n * f(n - 1);

忽略类型不看的话,这个 newFac0 跟最开始定义的 fac 简直一模一样!接下来就重新定义一下 Y 的类型,使其能与 FT 类型兼容:

delegate T YT<T> (FT<T> f);
delegate T SFT<T> (SFT<T> f);
SFT<Func<int, int>> sFT = f => f(f);
YT<Func<int, int>> yt = g => n => sFT (f => m => g(f(f)) (m)) (n); yt(newFac0)(5)
// 120

SFT 是一个辅助类型,因为 C# 里面不能直接调用 f => f(f) 这样的表达式。FT 是一个泛型的递归表达式的类型,可以用来定义任意的有递归能力的 Lambda。YT 定义了一个高阶函数的类型,可以用来递归调用一个匿名函数:

yt(f => n => n == 1 ? 1 : n * f(n - 1))(5)

再回过头去看最开始 fac 的使用方式: fac(fac)(5),如果我们把 facnewFac0 表示的 Lambda 表达式叫做 fn(f),其中 f = fn(f),这里出现了递归的定义,毕竟 fac 表示的是一个递归函数。也就是说 ffn 这个函数映射到了自身,这在数学上叫做“不动点”,例如 f(x) = x^2, 那么 x = 1 时,f(1) = 1,那么 x 就是函数 f 的一个不动点。

所以 yt(fn(f)) = fn(fn(f)) = fn(f) = f 好吧,其实这里我也有些混乱了

所以 yt(fn) 这个函数计算出了函数 fn(x) 一个不动点,也就是 f ,人们就把 yt 称为 不动点算子(factor) 也就是 Y Combinator。


参考链接:

https://blog.cassite.net/2017/09/09/y-combinator-derivation/

C# 函数式编程 —— 使用 Lambda 表达式编写递归函数的更多相关文章

  1. Java 函数式编程(Lambda表达式)与Stream API

    1 函数式编程 函数式编程(Functional Programming)是编程范式的一种.最常见的编程范式是命令式编程(Impera Programming),比如面向过程.面向对象编程都属于命令式 ...

  2. Java 函数式编程和Lambda表达式

    1.Java 8最重要的新特性 Lambda表达式.接口改进(默认方法)和批数据处理. 2.函数式编程 本质上来说,编程关注两个维度:数据和数据上的操作. 面向对象的编程泛型强调让操作围绕数据,这样可 ...

  3. Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...

  4. Java8函数式编程和lambda表达式

    文章目录函数式编程JDK8接口新特性函数接口方法引用函数式编程函数式编程更多时候是一种编程的思维方式,是一种方法论.函数式与命令式编程区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉 ...

  5. Java8函数式编程以及Lambda表达式

    第一章 认识Java8以及函数式编程 尽管距离Java8发布已经过去7.8年的时间,但时至今日仍然有许多公司.项目停留在Java7甚至更早的版本.即使已经开始使用Java8的项目,大多数程序员也仍然采 ...

  6. Python函数式编程:Lambda表达式

    首先我们要明白在编程语言中,表达式和语句的区别. 表达式是一个由变量.常量.有返回值的函数加运算符组成的一个式子,该式子是有返回值的 ,如  a + 1 就是个表达式, 单独的一个常量.变量 或函数调 ...

  7. 函数式编程--使用lambda表达式

    前面一篇博客我们已经说到了,lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口的实例.现在我们来写一段java的命令者模式来自己研究下lambda表达式的语法. 这里重复下命令者模式: ...

  8. Java函数式接口与Lambda表达式

    什么是函数式接口? 函数式接口是一种特殊的接口,接口中只有一个抽象方法. 函数式接口与Lambda表达式有什么关系? 当需要一个函数式接口的对象时,可以提供一个lambda表达式. package l ...

  9. Lambda01 编程范式、lambda表达式与匿名内部类、函数式接口、lambda表达式的写法

    1 编程范式 主要的编程范式有三种:命令式编程,声明式编程和函数式编程. 1.1 命令式编程 关注计算机执行的步骤,就是告诉计算机先做什么后做什么 1.2 声明式编程 表达程序的执行逻辑,就是告诉计算 ...

随机推荐

  1. 前端html 中jQuery实现对文本的搜索并把搜索相关内容显示出来

    做项目的时候有这么一个需求,客户信息显示出来后我要搜索查找相关的客户,并把相关的客户信息全部显示出来,因为一个客户全部信息我写在一个div里面  所以显示的时候就是显示整个div.先看看实现的效果: ...

  2. Visual Studio 生成DLL文件

    新建一个项目,在菜单栏中选择“项目”/“**属性”选项,该页面中将“输出类型”下拉列表中的选项选择为“类库”,然后重新生成一下该项目,或者在“Visual Studio 2008命令提示”中输入以下命 ...

  3. node笔记-node的好基友monggoDB

    mongoDB--非关系型数据库的佼佼者 mongodb是一个基于分布式文件存储的数据库,由c++语言编写. 特点:高性能.易部署.易使用. 下载地址:http://www.mongodb.org/d ...

  4. JavaWeb面试(七)

    61,JDBC访问数据库的基本步骤是什么?1,加载驱动2,通过DriverManager对象获取连接对象Connection3,通过连接对象获取会话4,通过会话进行数据的增删改查,封装对象5,关闭资源 ...

  5. C# 跨平台的支付类库ICanPay

    随着微软的开源,越来越多的项目支持跨平台,但是各种支付平台提供的类库,又老又不支持跨平台,吐槽下,尤其是微信,还有好多坑,于是ICanPay诞生了,今天就来讲ICanPay是什么,怎么使用? ICan ...

  6. 十三、Hadoop学习笔记————Hive安装先决条件以及部署

    内嵌模式,存储于本地的Derby数据库中,只支持单用户 本地模式,支持多用户多会话,例如存入mysql 下载解压hive后,进到conf路径,将模板拷贝 出现该错误表示权限不够 该目录未找到 新建一个 ...

  7. JAVA基础1——字节&位运算

    占用字节数 & 取值范围 Java一共有8种基本数据类型(原始数据类型): 类型 存储要求 范围(包含) 默认值 包装类 int 4字节(32位) -2^31~ 2^31-1 0 Intege ...

  8. HDU3792---Twin Prime Conjecture(树状数组)

    Twin Prime Conjecture Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Ot ...

  9. CCF-201412-2-Z字形扫描

    问题描述 试题编号: 201412-2 试题名称: Z字形扫描 时间限制: 2.0s 内存限制: 256.0MB 问题描述: 问题描述 在图像编码的算法中,需要将一个给定的方形矩阵进行Z字形扫描(Zi ...

  10. C# Dictionary根据Key排序

    using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Cons ...