匿名递归,即使用定点组合器,在命令式强类型语言中并不常见,原因很简单,命名[审查]函数比定义执行相同任务的匿名函数更容易。此外,OOA&D 告诉我们不应该重复在多个地方有价值的代码;它应该被命名,因此可以从一个共同的地方访问。Lambda 本质上是一次性的。一种指定几行非常特定于情况的代码以用于更通用的算法(如循环构造)的方法。大多数递归算法都是具有相当普遍应用的事物(排序、递归系列生成等),这通常会导致您更广泛地访问它们。
除了 Lambda 演算,在大多数编程语言中,匿名函数 F 必须存在才能使用。这排除了函数本身的定义。在一些函数式语言(例如 Erlang)中,函数 F 是使用“重载”定义的,其中更简单的函数用作更复杂函数的基本情况:
Fact(0) -> 1
Fact(i) -> Fact(i-1) * i
这很好,除了在 Erlang 世界中,这现在是一个命名函数“Fact”,当调用该方法时,程序将“跌倒”重载,直到找到参数匹配的第一个。由于 C# 不支持基于值选择重载,因此 C# 中没有与此确切构造等效的内容。
诀窍是以某种方式获得对可以传递给函数的函数的引用。有很多方法,所有这些方法都需要预先存在的参考。如果不能按名称引用函数,则 FP-combinator 函数的类型为Func<Func<Func<Func<Func<...
. Konrad 的方法是最简单的,但在 C# 中,它最终成为一种 hack(它可以编译,但 ReSharper 仍然抱怨它可能是 InvalidOperationException;不能调用空方法指针)。
这是我用于简单情况的东西,基本上使用委托解决方法来无法隐式键入隐式类型的 lambda:
public static class YCombinator
{
public delegate TOut RLambda<TIn, TOut>(RLambda<TIn, TOut> rLambda, TIn a);
public static Func<T,T> Curry<T>(this RLambda<T,T> rLambda)
{
return a => rLambda(rLambda, a);
}
}
//usage
var curriedLambda = YCombinator.Curry<int>((f, i) => i <= 0 ? 1 : f(f, i - 1)*i)
var shouldBe120 = curriedLambda(5);
您可以声明Curry<TIn, TOut>
重载来处理输入类型不是输出类型的情况,例如生成前 N 个素数的列表;该函数 P 可以递归地定义为生成所有正整数列表的函数,这些正整数不能被任何较小的素数整除。不动点 P(1) => 2 定义了一个基本情况,从中可以定义递归算法(尽管不是一个非常有效的算法):
var curriedLambda =
YCombinator.Curry<int, List<int>>(
(p, i) => i == 1
? new List<int>{2}
: p(p, i - 1)
.Concat(new[]
{
Enumerable.Range(p(p, i - 1)[i - 2],
int.MaxValue - p(p, i - 1)[i - 2])
.First(x => p(p, i - 1).All(y => x%y != 0))
}).ToList()
);
Assert.AreEqual(new []{2,3,5,7,11}, curriedLambda(5));
难题就这样出现了;尽管您当然可以将所有内容定义为高阶函数,但如果以命令式而不是函数式定义,这个素数查找器会快得多。主要的加速只是简单地在每个级别定义 p(p,i-1) ,因此每个递归级别不会评估 3 次。一种设计用于功能性工作的更智能的语言将为您做到这一点。