首页 > 代码库 > 11.1.1 重用常见的代码块

11.1.1 重用常见的代码块

11.1.1 重用常见的代码块

 

一个最好的编程实践,就是避免在多个地方重复相同的代码。如果有两个类似程序,就值得考虑把它们合并成一个;新的程序需要有新的参数,描述代码按照不同于原来的路径。

在函数式编程中,我们有一个强大的武器:函数值作为参数值使用的能力,这使得函数或者方法的参数化更容易。为了演示,假设我们有一个关于城市的信息数据库,我们要用数据生成几份报表。

我们先写一个加载数据的函数。为了使示例简单,我们不考虑使用数据库;当然,你可以自己去做,只要使用标准的.NET 数据库API,可顺利地使用F#。这里,我们就使用下面的函数,简单地返回我们手工创建的列表:

 

let loadPlaces() =

  [ ("Seattle",594210); ("Prague", 1188126)

    ("NewYork", 7180000); ("Grantchester", 552)

    ("Cambridge",117900) ]

 

这个数据结构虽然很简单,但已经接近于实际使用的应用程序。我们没有使用元组来保存名字和人口,而是可能使用记录或对象类型。清单11.1 是两个函数,从数据生成报表:一个输出超过一百万居民的城市名单,另一个输出按字母顺序所有城市。在实际应用程序中,可能会生成HTML 报表,但为了尽量简单,我们只以纯文本方式输出到控制台。

 

清单11.1 输出城市信息(F#)

let printBigCities() =

  letplaces = loadPlaces()

  printfn"===== Big cities ====="   [1] <--输出报表标题

  letselected = List.filter (fun (_, p) -> p > 1000000) places  [2] <-- 列出人口百万的城市

  forname, population in selected do

    printfn " - %s (%d)" name population

 

let printAllByName() =

  letplaces = loadPlaces()

  printfn"===== All by name ====="   [3]<-- 输出报表标题

  letselected = List.sortBy fst places   [4] <-- 按城市名字排序

  forname, population in selected do

    printfn" - %s (%d)" name population

 

这两个函数结构非常相似,但也有一些差异。最重要的不同是选择输出城市列表的方式。printBigCities 函数使用List.filter 来筛选城市[2],而printAllNames 使用List.sortBy 对城市列表重新排序[4];另外,输出的报表标题不同。

两者有许多共同的方面。首先,两个函数调用loadPlaces 以获得城市的集合;然后,以某种方式处理集合;最后,输出结果到屏幕。

重构这段代码,我们需要写一个的函数,能够用于这两项任务,还要让代码更具扩展性。使用完全不同策略的打印函数,应该是可能的。如果我们创建一个填字游戏,可能会寻找指定长度、有特定字母开头的城市。这样,我们应该能够提供几乎任何策略作为参数值。函数编程提供了一个很好的方法,使用函数实现这种参数化。

清单11.2 是高阶函数printPlaces,我们很快就会看到,它可以用前面清单中的两个函数来代替。

 

清单11.2 可重用的打印函数(F# Interactive)

> let printPlaces title select =

     letplaces = loadPlaces()

     printfn"== %s ==" title   [1]

     letsel = select(places)   [2]

     forname, pop in sel do

      printfn " - %s (%d)" name pop

;;

val printPlaces : string –>                     | [3]

  ((string* int) list -> #seq<string * int>) –> unit  |

 

新的函数有两个参数,指定原有两个函数彼此不同的地方。第一个是报表的标题[1],第二个是选择要打印城市的函数[2]。通过输出的类型签名[3],我们可以看到更多有关这个函数的类型信息。

这个函数的参数是元组的列表,元组由字符串和整数组成,这是表示城市的数据结构,与我们所期待的函数返回类型是相同的,因为函数以同样的数据格式返回城市的集合,但是,F# 推断出的类型是#seq<string * int>。差别只在于推断的类型是 #seq,而不是 list。

这种选择有两个重要的原因:

■seq<‘a> 是所有集合都实现的共同接口,是标准的.NET  IEnumerable<T>类型的别名。这样,函数可以返回列表,同样也可以返回数组,因为,我们唯一需要的是能够遍历集合中所有元素。在下一章,我们将更详细讨论序列,如果我们知道LINQ to Object,应熟悉这一领域:大多数常见的操作都使用(返回)IEnumerable <T>。

■# 号表明,返回的集合不必显式向上转换为seq<‘a> 类型,就是说,可以提供函数,它实际的类型返回list<‘a>。在严格意义上说,这是不同类型的函数,但是,# 号增加了一些重要的灵活性。大多数时候,不必非常担心,它只表示,编译器推断出,代码可能是泛型。

我们既然有了函数,需要展示它真正可以代替我们开头用的两个函数。清单11.3 表明。我们可以通过提供参数值,得到与原有函数相同的行为。

 

清单11.3 使用‘printPlaces’ 函数(F#)

// Writing lambda function explicitly

printPlaces "Big cities" (fun places–>     [1]

  List.filter(fun (_, s) -> s > 1000000) places)

 

// Using partial function application

printPlaces "Big cities" (List.filter(fun (_, s) -> s > 1000000))   [2]

printPlaces "Sorted by name" (List.sortByfst)    [3]

 

第一例子中唯一重要的方面是[1],第二个参数使用了lambda 函数。它把数据集作为参数值,使用List.filter 筛选出只超过100 万居民的城市;第二个例子表明[2],使用散函数应用,可以写出更简洁的调用;在最后一个例子中[3],使用List.sortBy 对集合进行排序。

在清单11.3 中可以看到,使用在重构过程中创建的函数是很容易的,只需要第二个参数指定不同的函数,就可以用来打印出不同的列表。

我们在这一节进行的重构,依靠使用函数作为参数的能力。C# 有相同的能力,因此,在这里,使用委托,可以有效地应用同样的重构。进行数据转换的参数,我们既可以指定为lambda 表达式,也可以从有合适签名的方法创建委托。

重构代码时的另一个重要的函数原则,是使用不可变数据。这里的影响,相比能够使用函数表达行为差异,要稍许有点微妙,但可能更重要。

11.1.1 重用常见的代码块