5.5.3 多参数值的函数
让我们回顾一下,写一个函数,能有哪些选项。在 F# 中,当写具有多个参数值的函数时,我们可以使用元组。我们下一个示例显示了一个函数,以这种风格加两个整数。我们将使用 lambda 函数的语法,但在 F# 中,使用简单的 let 绑定,也可以得到相同的结果:
> let add = fun (a, b) -> a + b;;
val add : int * int -> int
通过看类型签名,可以看到,该函数取一个参数值,这是一个元组的形式 (int * int),返回类型为 int。对应的 C# 的 lambda 函数,写成这样的形式:
Func<int, int, int> add =
(a, b) => a + b
Func < int、 int、 int > 委托表示一个方法,有两个 int 类型的参数值,并返回一个 int 类型,所以,这类似于使用元组写的 F# 版本。调用这个函数时,也可以看到这种相似性:
let n = add(39, 44)
var n = add(39, 44)
调用以元组作为参数值的 F# 函数(第一行),与调用 C# Func 委托(第二行)的语法是相同的。现在,让我们写相同的代码,在 F# 中,使用传统的 F# 样式,写有多个参数值的函数:
> let add = fun a b -> a + b;;
val add : int -> int -> int
令人惊讶的是,这与我们早先看到的在返回一个函数时的签名相同。可以读作, int-> (int-> int)。这就是一个函数,取第一个参数给这个加法,并返回一个函数。那么,结果是一个函数,取第二个参数值。我们可以重写这段代码,以这种方式,使用两个 lambda 函数,一个嵌套在另一个中:
> let add = fun a -> fun b -> a + b;;
val add : int -> int -> int
如果这是你第一次见到这种概念,它似乎很奇怪。一个函数返回另外一个函数,如何可以和函数,取另一个参数值并返回一个整数相同呢?有一个参数的函数如何可以和有两个参数的函数相同呢?不要太担心,如果不能马上理解——我们承诺,最终会理解的。在看过更多的示例之后,可能想转回到这一节,你就会更容易领会这个概念。
有多个元素的元组
如果要创建具有两个以上元素的元组,在 C# 中使用 .NET 4.0,可以使用重载的 Tuple 类,它提供重载表示的元组包含的元素从一个到八个。我们在第 3 章中实现的 C# 类有更多的限制,只支持仅 2 个元素。我们实现的重载,无论有多少个,总是会有一些限制。
不过,有一个方法可以克服这种限制。让我们看一下,我们如何可以使用来自第 3 章的 Tuple<A, B> 类型,表示 F# 的类型 int * string * bool。这个解决方案很简单,要存储比我们的元组类型支持的更多的元素,可以嵌套元组:
Tuple<int, Tuple<string, bool>> tup = (...);
当我们像这样声明一个变量时,它会携带三个值。要得到整型值,我们可以写成 tup.Item1;访问字符串值,写成 tup.tem2.Item1;最后,布尔值存储在 tup.Item2.Item2 中。
这是类似于嵌套函数,如 F# 类型 int -> (string -> bool)。在元组和函数之间有不同。此处所示的函数类型,与 int -> string –> bool 是同样的事情,而 F# 有三个元素的元组 (int * string * bool), 是不同于嵌套的元组类型,如 int * (string * bool)。
你可能想知道是否存在在 C# 3.0 中重写我们前面的示例的方法——确实可以。不要创建 Func<int, int, int> 类型的委托,可以创建一个 Func<int, Func<int, int>> 类型的委托,这更接近于 F# 函数的理解,签名为 int-> (int-> int):
Func<int, Func<int, int>> add =
a => b => a + b;
int n = add(39)(44);
写这个声明,用了两个 lambda 函数,就像我们前面的 F# 示例。当用此委托加数字时,我们必须调用第一个委托,它返回另一个委托;然后,调用第二个委托。在 F# 中,这是完全正常的、使用函数的方法,编译器会优化它,使之更有效率。
这是所有很有趣,但是,你可能会想,函数以这种方式的分离点是什么呢?原来是惊人的强大。
偏函数应用(PARTIAL FUNCTION APPLICATION)
要显示对函数的新理解是有用的情况,让我们把注意力返回到列表。假设我们有一个数字列表,我们想要为列表中的每个数字加上 10。在 F# 中,可使用 List.map 函数完成 ;在 C# 中,可以使用来自 LINQ 的 Select 方法:
list.Select(n => n + 10)
List.map (fun n -> n + 10) list
这已经相当简单了,但是还可以更简洁,如果我们已经有了来自前面示例中的 add 函数。List.map 期望作为第一个参数值的函数是 int -> int 类型。即,函数取一个整数作为参数值,并返回另一个整数。我们可以使用的技术称为部分偏函数应用(partial function application ):
> let add a b = a + b;;
val add : int -> int –> int
> let addTen = add 10;;
val addTen : (int -> int)
> List.map addTen [ 1 .. 10 ];;
val it : int list = [11; 12; 13; 14; 15; 16; 17; 18; 19; 20]
> List.map (add 10) [ 1 .. 10 ];;
val it : int list = [11; 12; 13; 14; 15; 16; 17; 18; 19; 20]
函数 add 的类型是 int -> int-> int。因为我们现在知道,它实际上意味着,该函数取一个整数并返回一个函数,因此,可以创建一个函数 addTen,把 10 加到给定的参数值上,通过调用 add,只有第一个参数值。然后,可以使用此函数作为参数值,给 List.map 函数。这有时很有用,但更重要的是,我们可以直接使用偏函数应用,在为 List.map 函数指定第一个参数值时。
函数 add 的类型是 int-> (int-> int),通过用一个数字作为参数值调用它,得到的结果类型是 int-> int,这正是 List.map 函数所需要的。当然,我们可以在 C# 中写相同的代码,如果使用嵌套的 lambda 函数声明 add 函数:
Func<int, Func<int, int>> add =
a => b => a + b;
list.Select(add(10));
正如我们在 F# 版本中看到的,调用 add 委托,得到结果类型 Func<int, int>,这与 Select 方法兼容。在 C# 中,使用具有多个参数值的 Func 委托,并使用另一个 lambda 函数为 Select 方法指定参数值,是更方便的,因为,语言支持更好。
注意
在使用偏函数应用时,会听到一个术语科里化(currying) 。它指的是,转换有多个参数值作(比如元组)的函数,到有第一个参数值的函数,并返回一个函数,有其余的参数,等等。例如,类型 int -> int-> int 的函数是(int * int)-> int 类型函数的科里化形式。那么,偏函数应用就是不指定所有参数值的科里化函数的使用。
正如我们已经提到,F# 中选择正确的样式可能很难。使用元组写的代码有时更容易读取大量的参数值,但它不能于偏函数应用。在这本书的其余部分,我们将使用感觉更适合各种情况的风格,这样,就可以获得的直观地理解哪个更好。最重要的是,我们使用元组,是在使代码更具可读性的情况下,和允许偏函数应用的风格,这种情况为我们提供了明显的好处。在下一章讨论高阶函数时,我们将会看到有关后者的很多例子。