7.3 柯里(Curried)函数:针对部分应用进行了优化
以数学家Haskell Curry的名字命名,柯里化是将一个接受参数t1, t2, …, tn的n次方函数f转化为一个接受t1并产生一个接受takest2的新函数,以此类推,最终在参数全部给出后返回与f相同的结果。
换句话说,一个带有签名的 n 元函数
(T1, T2, …, Tn) -> R
Curried的时候,具有标志性的
T1 -> T2 -> … -> Tn -> R
你已经在本章的第一部分看到了一个例子:
Func<Greeting, Name, PersonalizedGreeting> greet
= (gr, name) => $"{gr}, {name}";
Func<Greeting, Func<Name, PersonalizedGreeting>> greetWith
= gr => name => $"{gr}, {name}";
我提到过greetWith 就像greet一样,但是采用了柯里的形式。确实,比较签名:
greet : (Greeting, Name) -> PersonalizedGreeting
greetWith : Greeting -> Name -> PersonalizedGreeting
这意味着你可以像这样调用 greetWith 柯里函数:
greetWith("hello")("world") // => "hello, world"
这是两个函数的调用,它实际上与调用有两个参数的greet相同。当然,如果你打算同时传入所有的参数,这就相当没有意义了。但当你对部分应用感兴趣时,它就变得有用了。
如果一个函数是柯里的,那么部分应用只需通过调用函数来实现:
var greetFormally = greetWith("Good evening");
names.Map(greetFormally).ForEach(WriteLine);
// prints: Good evening, Tristan
// Good evening, Ivan
一个函数可以用柯里的形式编写,就像这里的greetWith,这叫做手动柯里化。或者,可以定义通用函数,这些函数将采用 n 元函数并对其进行柯里化。对于二元和三元函数,柯里如下所示:
public static Func<T1, Func<T2, R>> Curry<T1, T2, R>
(this Func<T1, T2, R> func)
=> t1 => t2 => func(t1, t2);
public static Func<T1, Func<T2, Func<T3, R>>> Curry<T1, T2, T3, R>
(this Func<T1, T2, T3, R> func)
=> t1 => t2 => t3 => func(t1, t2, t3);
类似的重载也可以为其他数组的函数定义。 作为一个练习,请用箭头符号写出前面的函数的签名。
让我们看看如何使用这样一个通用的柯里函数来对 greet 函数进行柯里化:
var greetWith = greet.Curry();
var greetNostalgically = greetWith("Arrivederci");
names.Map(greetNostalgically).ForEach(WriteLine);
// prints: Arrivederci, Tristan
// Arrivederci, Ivan
当然,如果你想使用通用的柯里函数,关于方法解析的注意事项和Apply一样适用。
部分应用和柯里化是密切相关但又截然不同的概念,当您介绍它们时,这通常会令人困惑。让我们阐明差异:
- 部分应用 —— 你给一个函数的参数少于函数期望的参数,得到的是一个以目前所给的参数值为特征的函数。
- 柯里化 —— 你没有提出任何论点;您只需将一个 n 元函数转换为一个一元函数,然后可以连续给它传递参数以最终获得与原始函数相同的结果。
正如你所看到的,柯里化并没有真正做任何事情。相反,它为部分应用程序“优化”了一个功能。正如我们在本章前面所做的那样,您可以使用通用的 Apply 函数进行部分应用而无需柯里化。另一方面,柯里化本身是没有意义的:你柯里化一个函数(或以柯里化形式编写一个函数),这样你就可以更容易地使用部分应用程序。
部分应用程序在 FP 中非常常用,以至于在许多函数式语言中,默认情况下所有函数都是柯里化的。出于这个原因,箭头符号中的函数签名在 FP 文献中以柯里化形式给出,如下所示:
T1 -> T2 -> … -> Tn -> R
在本书的其余部分,我将始终使用柯里化符号,即使对于实际上没有柯里化的函数也是如此。
尽管函数在C#中不是默认柯里化的,但你仍然可以利用部分应用的优势,允许你通过参数化它们的行为来编写高度通用的、因而可广泛重用的函数,然后使用部分应用来创建你时常需要的更具体的函数。
正如您目前所见,您可以通过不同的方式实现这一点:
- 通过以柯里化形式编写函数
- 通过使用 Curry 对函数进行柯里化,然后使用后续参数调用柯里化函数
- 通过 Apply 一一提供参数
你使用哪种技术是一个喜好问题,尽管我个人发现使用Apply是最直观的。