6.7.2 理解列表函数的类型签名
我前面提到过,我们已用函数来筛选和映射列表,但使用都很直观。在本节中,我们将看看它们的类型签名,会看到,我们可以推断一个高阶函数只使用此信息做什么。
当然,在一般情况下,你不能通过看函数的类型,就知道它能做什么,但对于泛型和高阶函数,例如那些用来处理列表的函数,通常是可能的。如我们前面所见的,处理泛型值的函数则不能单独处理值太多,因为,它们不知道关于值的所有事。其结果是,它们通常取一个函数作为额外的参数值,并使用它来处理这个值。
函数的类型提供了一些线索,结果将如何使用。让我们演示使用类型签名,显示在清单 6.20 中。
Listing 6.20 Types of functions and methods for working with lists (F# and C#)
// F# function signatures
List.map : ('a -> 'b) -> 'a list -> 'b list
List.filter : ('a -> bool) -> 'a list -> 'a list
// C# method declarations
List<B> Select<A, B> (List<A>, Func<A, B>)
List<A> Where<A> (List<A>, Func<A, bool>)
让我们先看看映射。如你所见,输入的参数是类型为 'a 的值的列表,结果是类型为 'b 的值的列表 。该操作不会知道 'b 是什么,所以,它不能单独创建此类型的值。创建一个类型的 'b 的值的唯一方法,是使用一个给定函数作为参数值,转换类型为 'a 的值' 为类型为 'b 的值。这表明,操作能运行的唯一合理方法,是遍历输入列表中的值,为每个值调用这个函数,并返回结果列表。事实上,这正是映射操作所做的事。
值得注意的是,在这种情况下,输入列表的类型和输出列表的类型可以不同。第 5 章中,我们在整数列表中加一个数字 10,因此,输入列表与输出列表的类型相同。我们可以使用一个函数,从一个数字参数值创建字符串。在这种情况下,输入的列表是整数列表,而结果是字符串列表。
第二个操作中筛选。这里,输入和结果列表的类型相同。给定的函数作为参数,是一个断言,返回 true 或 false,类型为 'a 的值,与输入列表中元素的类型相同。这给了我们一个好的提示,该操作可能为列表中的每个元素调用这个函数,和使用这个结果,来确定是否应将该元素复制到返回的列表中。
处理列表
让我们看一个更大的示例,显示了使用筛选和映射。这两个在 F# 库中都可用,适用各种集合类型,但我们将使用列表,因为,我们已经很熟悉。在 C# 中,这些方法可用于任何实现了 IEnumerable<T> 的集合,所以,我们将使用泛型 .NET List <T> 类。清单 6.21 显示了我们将要处理的数据的初始化。
Listing 6.21 Data about settlements (C# and F#)
// C# version using a simple class
class CityInfo {
public CityInfo(string name, int population) {
Name = name; Population = population;
}
public string Name { get; private set; }
public int Population { get; private set; }
}
var places = new List<CityInfo> { new CityInfo("Seattle", 594210),
new CityInfo("Prague", 1188126), new CityInfo("New York", 7180000),
new CityInfo("Grantchester", 552), new CityInfo("Cambridge", 117900) };
// F# version using tuples
> let places =
[ ("Seattle", 594210); ("Prague", 1188126); ("New York", 7180000);
("Grantchester", 552); ("Cambridge", 117900) ];;
val places : (string * int) list
在 F # 中,我们将使用常见的例子,有关城市的信息的列表,有名字和人口。虽然,我们可以转换这个 F# 元组到我们已经实现了的 Tuple 类,但这一次,我们会使用更典型 C# 表示。声明CityInfo 类,并使用它来创建一个包含城市信息的列表。
在 C# 中,我们可以处理这个数据,使用在 .NET 3.5 中可用的 Where 和 Select 方法。这两个都是扩展方法,因此,我们可以用通常的点表示法调用它们:
var names =
places.Where(city => city.Population > 1000000)
.Select(city => city.Name);
另外,这还显示了使用高阶操作的优点。这个给定的 lambda 函数作为参数值,指定了筛选的条件(在第一个情况下),或为每个城市(在第二种情况)指定返回值。这是我们必须指定的。我们不需要了解集合的基础结构,我们不指定应该如何获得结果。所有这些被封装在这个高阶操作中。
让我们在 F# 中执行相同的操作。首先,想要筛选数据集,然后,只选择城市的名字。我们可以通过调用 List.filter,并用这个结果作为最后的参数给 List.map 函数。正如你所看到的,这看起来很丑陋,而且难于阅读:
let names =
List.map fst
(List.filter (fun (_, pop) -> 1000000 < pop) places)
当然,F# 可以做得比这更好。前面的 C# 版本是优雅的,因为,我们可以写操作的顺序,它们执行(先筛选,再映射)的顺序相同,可以每个操作写在单独的一行上。在 F# 中,我们使用流,可以获得相同代码布局:
let names =
places |> List.filter (fun (_, pop) -> 1000000 < pop)
|> List.map fst
在这种情况下,流运算符首先将左边的值(places) 传递到右边的筛选函数。下一步,第一个操作的结果传递给下一个操作 (这里是映射)。尽管我们使用这个运算符,有相当一段时间了,但是,这个示例最后显示了为什么它会被称为"流"。数据元素被依次处理,好像它们经过管道一样,并且,这个管道是由使用流运算符通过链接起来的几个操作创建。
注意,有时操作的顺序是重要的,有时不重要。这里,我们必须先执行筛选。如果我们在第一步先做映射,会获得仅包含城市名字的列表,就不会有关于人口的信息,这是进行筛选需要的。
当在 F# 中写列表处理,可以把流和其他函数技术组合起来,比如偏函数应用和函数组合。让我们简单地看看下一个步骤,在写处理代码时产生的:
let names =
places |> List.filter (snd >> ((<) 1000000))
|> List.map fst
不是指定显式使用 lambda 表达式的筛选函数,而是使用函数组合生成。第一个函数是 snd,它返回元组的第二个元素。我们这里,它是表示人口的数字。在组合中使用的第二个函数,是一个偏应用运算符。我们仅指定第一个参数值,因此,会得到一个函数,当第二个参数是大于给定数字时,返回 true。
提示
写代码(不只是在函数风格中)时,应该始终考虑到理解代码,及需要对其进行修改,是多么困难。在前面的示例中,使用函数组合的版本并不是特别简短,看上去也不更优雅。事实上,我们觉得其可读性比显式 lambda 函数的版本要差,因此,在这种情况下,我们更倾向于使用 lambda 表示法。很多情况下,函数组合能显著简化代码。不幸的是,没有简单的规则可遵循。我们可以给你的最好的忠告,是使用常识,并想象有其他人还试图理解这段代码。
处理数据的集合是我们经常做的任务,因此,编程语言设计者尝试使其尽可能容易。现在,C# 和 F# 都提供了更方便的方式,可以解决相同的任务,我们只要用高阶函数实现这个任务。理解高阶函数的工作是必要的,因为,可以使用它们来处理任何数据结构,而不仅仅是列表。
C# 3.0 的查询和 F# 的序列表达式
你可能已经看到,在 C# 中使用查询表达式(query expressions)写数据查询的示例。使用此功能,我们前面的代码看起来,像这样:
var names = from p in places
where 1000000 < p.Population
select p.Name
这经常作为一项重要的新功能来演示,但它不会没有底层机制,比如 lambda 函数和高阶操作。我们已经明确关注这些,因为,当你学习显式使用它们时,可以使用一种类似的函数方法,来处理任何数据,而不仅是集合。简化的语法非常有用,类似的功能在 F# 中也可用,被称为序列表达式(sequence expressions)。我们将在第 12 章讨论有关内容,但只是为了好奇的人,这里是用 F# 写的相同的查询:
let names =
seq { for (name, pop) in places do
if (1000000 < pop) then yield name }
就像括在一起的普通代码块,用词 seq 标记。这是意图,因为在 F# 中,它是更普遍的语言构造,也可以用于处理其他值。在第 12 章中,我们将看到如何使用它来处理选项值,但还将看到 C# 查询表达式如何用于类似的目的。
已经看过如何使用两个最常见的列表处理函数,并看到它们如何有用,让我们更深入看看第三个这样的函数,我们自己来实现。