6.7.3 实现列表函数
不显示如何实现刚才我们看到的筛选和映射函数,先来看看从第 3 章开始创建的函数。因为,所有列表处理函数都有类似的结构,你在看下面的示例后,可以能够实现其他任何的函数。
在第 3 章,我们写了一个函数,它可以列表中的所有元素求和或求积。后来,我们就意识到它是比首次出现时更有用:我们看到,它也能用来查找最小或最大元素。那时,我们没有讨论过泛型,因此,这个函数只处理整数。在清单 6.22 中,我们
看一个类似的函数,没有原先限制自动泛型化的类型批注。
Listing 6.22 Generic list aggregation (F# Interactive)
> let rec fold f init list =
match list with
| [] –> init
| head::tail –>
let state = f init head
fold f state tail
;;
val fold : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
这个实现非常像第 3 章中的那个。更重要的是,我们去掉了类型注释,因此,推断的签名是更通用的。该函数现在取一个列表,其值的类型为 'b,由聚合产生的值可以有不同类型(类型参数为 'a)。这个处理函数取当前的聚合结果(类型为 'a),和来自列表中的上一个元素('b),并返回一个新的聚合的结果。
我们将很快看到,泛型的使用使得聚合更有用。它在 F# 库中也可用。处理不可变的 F# 列表类型的版本位于 List 模块中。下面的代码片断显示了我们从原来的第 3 章中的用法,求列表中的所有元素的积:
> [ 1 .. 5 ] |> List.fold (*) 1
val it : int = 120
由于我们将要处理泛型函数,编译器必须首先推断出类型参数的类型。这里,我们处理的是整数列表,所以,参数 'b 是 int,结果也是整数,所以 'a 也是 int。清单 6.23 显示其他一些使用 fold 的有趣例子。
Listing 6.23 Examples of using fold (F# Interactive)
> places |> List.fold (fun sum (_, pop) -> sum + pop) 0;;
val it : int = 9080788
> places |> List.fold (fun s (n, _) -> s + n + ", ") "";;
val it : string =
"Seattle, Prague, New York, Grantchester, Cambridge, "
> places
|> List.fold (fun (b,str) (name, _) –>
let n = if b then name.PadRight(20) else name + "/n"
(not b, str+n)
) (true, "")
|> snd
|> printfn "%s";;
Seattle Prague
New York Grantchester
Cambridge
在所有示例中,我们处理的都是有关城市信息的集合,因此,列表的类型总是相同的。这意味着,参数 'b 的实际类型总是 (string * int) 元组。然而,聚合的结果不同。在第一种情况下, 我们只求人口的和,因此,结果的类型是 int。在第二个示例中,我们要建立有城市名字的字符串,因此,我们开始的聚合是空字符串。作为第一个参数值的 lambda 函数将追加当前处理的城市名字和分隔符。
在最后一个示例中,我们实现了改进格式的版本,将城市名字写成两列。这意味着,lambda 函数执行两个交替操作。第一种情况,用空格填充名字(填充第一列),在第二种情况中,则将添加换行符(以结束行)。这通过使用 bool 类型的临时值完成,最初设置为 true,然后,在每次迭代中切换。聚合值包含这个交替的临时值,和作为结果的字符串,因此,在结束时,我们需要从元组中删除临时值。
在 C# 中实现 fold
与 fold 有相同行为的操作,在 .NET 库中也是可用的。虽然,它的名字叫Aggregate(聚合) 。通常,它是用作扩展方法,工作任何集合类型上,我们可以使用它,与在 F# 函数相同的方式。让我们在 C# 3.0 中重写来自清单 6.21 的上一个示例。在 F# 中,我们用一个元组来存储在聚合过程中的状态。你也许还记得以前的几章中,我们提到过,C# 3.0 的匿名类型有时可以用于同一目的。这是一个真的非常适合它们的示例:
var res =
places.Aggregate(new { StartOfLine = true, Result = "" },
(r, pl) => {
var n = r.StartOfLine ? pl.Name.PadRight(20) : (pl.Name + "/n");
return new { StartOfLine = !r.StartOfLine, Result = r.Result + n };
}).Result;
在 C# 中,初始的值被指定为第一个参数值。我们创建一个匿名类型,有一个标志 StartOfLine(用作临时值), 和属性Result,其中存储连接的字符串。作为第二个参数值的 lambda 函数,做与前面的 F# 示例同样的事,但返回的结果也是匿名类型,与初始值具有相同的结构。为使代码更多有效,我们也可以使用 StringBuilder 类代替连接字符串,但是,我们想要显示最简单的可能的示例。
现在,已经知道了如何在 C# 中使用该函数,我们应该看看它是如何实现的。在清单 6.24 中,可以看到两个实现。其中一个是一个典型函数式实现,处理来自第 3 章的函数列表,另一个是命令式实现,处理泛型的 .NET List 类型,原则上,它与 .NET 库中的聚合的扩展方法相同。
Listing 6.24 Functional and imperative implementation of Fold (C#)
// Functional implementation using 'cons list'
R Fold(this FuncList list, Func func, R init) {
if (list.IsEmpty)
return init;
else {
var state = func(init, list.Head)
return list.Tail.Fold(func, state);
}
}
// Imperative implementation using 'List'
R Fold(this List list, Func func, R init) {
R temp = init;
foreach(var item in list)
temp = func(temp, item);
return temp;
}
除了使用不同的集合类型之外,这两个方法的签名是一样。它对应于前面 F# 中的声明,虽然,我们必须显式地写出类型参数。在这两种情况下,我们使用列表作为第一个参数,方法被实现为处理集合类型的扩展。
在函数式的版本中,们有两个分支。第一个是处理空列表的情况,第二个分支递归地处理 cons cell,并使用 fun(函数)参数聚合结果。命令式版本,声明一个本地可变值,存储聚合过程中的当前结果。聚合的值通过遍历的所有元素来计算,并在每个迭代中更新这个值。
正如我们已经提到的,其他操作的实现有非常类似的过程。在 map 和 filter 的函数式版本中,return init; 可能返回一个空列表,在命令式版本中,可以使用可变列表作为临时值,其他改变在这两行:R Fold(… 和 temp = func(temp, item);。当执行映射时,只要调用给定的函数,而对于筛选,可能决定是否要追加当前元素。
要结束我们关于高阶函数的讨论,将重点介绍几个函数之间有趣的关系,用来操作列表,和用来处理选项值的函数。