使用Lambda表达式编写递归函数

开发 后端
这里将介绍如何使用Lambda表达式编写递归函数,老赵在这里会从“伪”递归开始讲解,希望对大家理解Lambda表达式有所帮助。
Lambda表达式实现递归表达,在.NET编程中具有很重要的意义。递归可以更简便循环过程,但这里需要从“伪”递归开始谈起。

其实这从来不是一个很简单的事情,虽然有些朋友认为这很简单。

“伪”递归

例如,我们想要使用Lambda表达式编写一个计算递归的fac函数,一开始我们总会设法这样做:

Func fac = x => x <= 1 ? 1 : x * fac(x - 1); 
  • 1.

不过此时编译器会无情地告诉我们,fac还没有定义。于是您可能会想,这个简单,分两行写咯。于是有朋友就会给出这样的代码:

Func fac = null;  
fac = x => x <= 1 ? 1 : x * fac(x - 1); 
  • 1.
  • 2.

这样看起来也很“递归”,执行起来似乎也没有问题。但是,其实这并没有使用Lambda表达式构造一个递归函数,为什么呢?因为我们使用Lambda表达式构造的其实只是一个普通的匿名方法,它是这样的:

x => x <= 1 ? 1 : x * fac(x - 1); 
  • 1.

既然是“匿名方法”,这个构造的东西是没有名字的——因此用Lambda表达式写递归“从来不是一个很简单的事情”。那么这个Lambda表达式里的fac是什么呢?是一个“委托”。因此,这只是个“调用了一个委托”的Lambda表达式。“委托对象”和“匿名方法”是有区别的,前者是一个实际的对象,而后者只是个“定义方式”,只是“委托对象”可以成为“匿名方法”的载体而已。这个Lambda表达式构造的“委托对象”在调用时,它会去寻找fac这个引用所指向的委托对象。请注意,这里是根据“引用”去找“对象”,这意味着Lambda表达式构造的委托对象在调用时,fac可能已经不再指向当初的委托对象了。例如:

Func fac = null;  
fac = x => x <= 1 ? 1 : x * fac(x - 1);  
Console.WriteLine(fac(5)); // 120;  
 
Func facAlias = fac;  
fac = x => x;  
Console.WriteLine(facAlias(5)); // 20 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

***次打印出的120是正确的结果。不过facAlias从fac那里“接过”了使用Lambda表达式构造的委托对象之后,我们让fac引用指向了新的匿名方法x => x。于是facAlias在调用时:

facAlias(5)     <— facAlias是x => x <= 1 ? 1 : x * fac(x – 1)  
= 5 <= 1 ? 1 : 5 * fac(5 - 1)  
= 5 * fac(4)    <— 注意此时fac是x => x 
5 * 4 
20 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

自然就不对了。

因此,使用Lambda表达式构造一个递归函数不是一件容易的事情。把自己传给自己吧

可能已经有朋友知道“标准”的做法是什么样的,不过我这里还想谈一下我当时遇到这个问题时想到的一个做法。比较笨(非常符合我的特点),但是可以解决问题。

  我的想法是,既然使用“Lambda表达式来构造一个递归函数”的难点是因为“我们正在构造的东西是没有名字的”,因此“我们无法调用自身”。那么,如果我们换种写法,把我们正在调用的匿名函数作为参数传给自己,那么不就可以在匿名函数的方法体中,通过调用参数来调用自身了吗?于是,原本我们构造fac方法的Lambda表达式:

x => x <= 1 ? 1 : x * fac(x - 1); 
  • 1.

就需要变成:

(f, x) => x <= 1 ? 1 : x * f(f, x - 1); 
  • 1.

请注意,这里的f参数是一个函数,它也是我们正在使用Lambda表达式定义的匿名委托(就是“(f, x) => ...”这一长串)。为了递归调用,它还必须把自身作为***个参数传入下一层的调用中去,所以***不是fac(x - 1)而是f(f, x - 1)。我们可以把这个匿名函数放到一个叫做selfFac的变量中去:

var selfFac = (f, x) => x <= 1 ? 1 : x * f(f, x - 1); 
  • 1.

在***次调用selfFac时,我们必须把它自身传递进去。于是我们可以这样来获得阶乘的结果:

Console.WriteLine(selfFac(selfFac, 5)); // 120; 
  • 1.

但是这段代码没法编译通过,因为编译器不知道selfFac应该是什么类型的委托对象。不过根据selfFac(selfFac, 5)的调用方式,我们可以推断出,这个委托类型会接受两个参数,***个是它自身的类型,第二个是个整型,而返回的也是个整型。于是,我们可以得出委托的签名了:

delegate int SelfFactorial(SelfFactorial selfFac, int x); 
  • 1.

哎,但是这一点都不通用啊。没关系,我们可以写的通用一些:

delegate TResult SelfApplicable(SelfApplicable self, T arg); 
  • 1.

这样,我们便可以定义selfFac,甚至于selfFib(菲波纳契数列):

SelfApplicable selfFib = (f, x) => x <= 1 ? 1 : f(f, x - 1) + f(f, x - 2);  
Console.WriteLine(selfFib(selfFib, 5)); // 8 
  • 1.
  • 2.

但是,这还不是我们所需要的递归函数啊。没错,我们需要的是传入一个x就可以得到结果的函数,这种每次还需要把自己传进去的东西算什么?不过这个倒也容易,在selfXxx的基础上再前进一步就可以了:

SelfApplicable selfFac = (f, x) => x <= 1 ? 1 : x * f(f, x - 1);  
Func fac = x => selfFac(selfFac, x);  
 
SelfApplicable selfFib = (f, x) => x <= 1 ? 1 : f(f, x - 1) + f(f, x - 2);  
Func fib = x => selfFib(selfFib, x); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

为此,我们甚至可以总结出一个辅助方法:

static Func Make(SelfApplicable self)  
{  
    return x => self(self, x);  
}  于是乎:  
 
var fac = Make((f, x) => x <= 1 ? 1 : x * f(f, x - 1));  
var fib = Make((f, x) => x <= 1 ? 1 : f(f, x - 1) + f(f, x - 2)); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样我们便使用Lambda表达式定义了递归函数。当然,需要两个参数的递归函数定义方式也比较类似。首先是SelfApplicable委托类型和对应的辅助方法:

// 委托类型  
delegate TResult SelfApplicable(SelfApplicable self, T1 arg1, T2 arg2);  
 
// 辅助方法  
static Func Make(SelfApplicable self)  
{  
    return (x, y) => self(self, x, y);  
}  于是使用“辗转相除法”计算***公约数的gcd函数便是:  
 
var gcd = Make((f, x, y) => y == 0 ? x : f(f, y, x % y));  
Console.WriteLine(gcd(20, 36)); // 4 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

这也是我目前凭“个人能力”能够走出的最远距离了。

不动点组合子

但是装配脑袋很早给了我们更好的解决方法:

static Func Fix(Func, Func> f)  
{  
    return x => f(Fix(f))(x);  
}  
 
static Func Fix(Func, Func> f)  
{  
    return (x, y) => f(Fix(f))(x, y);  

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

Fix求出的是函数f的不动点,它就是我们所需要的递归函数:

var fac = Fix(f => x => x <= 1 ? 1 : x * f(x - 1));  
var fib = Fix(f => x => x <= 1 ? 1 : f(x - 1) + f(x - 2));  
var gcd = Fix(f => (x, y) => y == 0 ? x : f(y, x % y)); 
  • 1.
  • 2.
  • 3.

用脑袋的话来说,Fix方法应该被视为是内置方法。您比较Fix方法内部和之前的Make方法内部的写法,就能够意识到两种做法之间的差距了。

由于我的脑袋不如装配脑袋的脑袋装配的那么好,即使看来一些推导过程之后还是无法做到100%的理解,我还需要阅读更多的内容。希望在以后的某一天,我可以把这部分内容融会贯通地理解下来,并且可以详细地解释给大家听。在这之前,我还是听脑袋的话,把Fix强行记在脑袋里吧。

***,希望大家多多参与一些如“函数式链表快速排序”这样的趣味编程——不过,千万不要学脑袋这样做:

var qsort = Fix, IEnumerable>(f => l => 
l.Any() ? f(l.Skip(1).Where(e => e < l.First())).Concat(Enumerable.Repeat(l.First(), 1)).Concat(f(l.Skip(1).Where(e => e >= l.First()))) : Enumerable.Empty()); 
  • 1.
  • 2.

当然,偶尔玩玩是有益无害的。

本文来自赵劼博客园文章《使用Lambda表达式编写递归函数

【编辑推荐】

  1. .NET Lambda表达式的函数式特性:索引示例
  2. .NET Lambda表达式的语义:字符串列表范例
  3. 使用.NET 3.5 Lambda表达式实现委托
  4. 各版本.NET委托的写法回顾
  5. C# Actor模型开发实例:网络爬虫
责任编辑:彭凡 来源: 博客园
相关推荐

2013-04-10 10:58:19

LambdaC#

2009-10-12 10:11:08

Lambda表达式编写

2013-04-10 10:46:06

LambdaC#

2009-08-10 09:41:07

.NET Lambda

2020-10-16 06:40:25

C++匿名函数

2021-08-31 07:19:41

Lambda表达式C#

2009-09-09 13:01:33

LINQ Lambda

2022-12-05 09:31:51

接口lambda表达式

2009-09-15 15:18:00

Linq Lambda

2009-09-11 09:48:27

Linq Lambda

2023-11-02 08:25:58

C++Lambda

2009-09-17 09:44:54

Linq Lambda

2009-09-15 17:30:00

Linq Lambda

2009-09-17 10:40:22

Linq Lambda

2009-08-27 09:44:59

C# Lambda表达

2012-06-26 10:03:58

JavaJava 8lambda

2024-03-25 13:46:12

C#Lambda编程

2009-04-29 09:05:59

Lambda抽象代表.NET

2009-12-14 09:57:04

Lambda表达式

2009-08-10 10:06:10

.NET Lambda
点赞
收藏

51CTO技术栈公众号