第七章 F# 库(三)
序列(Microsoft.FSharp.Collections.Seq)模块
Microsoft.FSharp.Collections.Seq 模块包含所有处理集合的模块,只要它支持 IEnumerable 接口, .NET 框架的 BCL 中的大多数集合都是的。这个模块之所以称为序列(Seq),是因为序列是IEnumerable 接口的别名,是对其简称,为了读、写更方便。给定类型定义时使用这个别名。
注意
FSLib 包含几个模块,用于处理不同类型的集合,包括Array(数组)、Array2(二维数组)、(三维数组)、Hashtbl(哈希表)、IEnumerable、LazyList、List、Map 和 Set。我们只讨论序列,是由于它有能力处理大量不同类型的集合,因此,通常更受青睐。另外,虽然每一种模块都有它们专门的函数,但是,仍有许多函数是共有的。
这些函数的一部分可以用列表推导(list comprehension)语法替换,我们在第三、四章有过讨论。对于简单的任务和非类型化的集合(untyped collections),保用列表推导更简单;但是,对于更复杂的任务,还是要使用这些函数。
map 和 iter:这两个函数把给定的函数应用到集合中的每一项;
concat:这个函数把集合的集合连接成一个集合;
fold:这个函数通过把集合中的项集中到一起,创建汇总;
exists 和 forall:这两个函数判断集合的内容;
filter, find 和 tryFind:这些函数挑选集合中符合条件的元素;
choose:这个函数同时执行筛选和映射;
init 和 initInfinite:这两个函数初始化集合;
unfold:这个函数以更灵活的方法初始化集合;
cast:这个函数转换 IEnumerable 的非泛型版本,而不是 IEnumerable<T>。
map 和 iter 函数
我们首选看一下map 和 iter 函数,这两个函数把给定的函数应用到集合中的每一项。它们之间的不同在于,map 是通过转换集合中的每一个元素,创建一个新的集合;而 iter 是把一个有副作用的运算应用到集合中的每一项。典型的副作用就是把元素写到控制台。下面的示例演示了map 和 iter 函数的应用:
let myArray = [|1; 2; 3|]
let myNewCollection =
myArray |>
Seq.map (fun x -> x * 2)
printfn "%A" myArray
myNewCollection |> Seq.iter (fun x ->printf "%i ... " x)
代码的运行结果如下:
[|1; 2; 3|]
2 ... 4 ... 6 ...
concat 函数
前面的示例使用了数组(array),因为这种集合类型初始化很方便,实际上,是可以使用 BCL 中所有的集合类型。下面的示例使用System.Collections.Generic 命名空间下的列表(List)类型,来演示concat 函数的用法,其类型为#seq< #seq<‘a> > -> seq<‘a>,将可枚举(IEnumerable)值的集合连接成一个可枚举值:
open System.Collections.Generic
let myList =
lettemp = new List<int[]>()
temp.Add([|1; 2; 3|])
temp.Add([|4; 5; 6|])
temp.Add([|7; 8; 9|])
temp
let myCompleteList = Seq.concat myList
myCompleteList |> Seq.iter (fun x ->printf "%i ... " x)
代码的运行结果如下:
1 ... 2 ... 3 ... 4 ... 5 ... 6 ... 7 ... 8... 9 ...
fold 函数
下面的示例演示 fold 函数,它的类型为(‘b -> ‘a -> ‘b) -> ‘b -> #seq<‘a> ->‘b。这个函数在整个函数调用过程中,使用一个累加器值,产生集合的汇总。函数有两个参数,第一个参数是逻辑器,它是前一个函数的结果,第二个参数集合中元素;函数体组合这两个值,产生一个新的值,其类型与累加器相同。在下面的示例中,myPhrase 的元素连接到一个累加器中,这样,所有的字符串最后形成一个字符串。
let myPhrase = [|"How";"do"; "you"; "do?"|]
let myCompletePhrase =
myPhrase |>
Seq.fold (fun acc x -> acc + " " + x) ""
printfn "%s" myCompletePhrase
代码的运行结果如下:
How do you do?
exists 和 forall 函数
下面的示例演示了两个函数,能够用来确定集合的内容。这两个函数的类型都是(‘a -> bool) -> #seq<‘a> ->bool。exists 函数确定集合中任意元素是否满足特定的条件;必须满足的条件由传递给exists 的函数决定,如果有任意的元素满足条件,exists 就返回真;forall 函数也很相似,但不同的是,必须集合中的所有元素都满足条件,才返回真。下面的示例第使用exists 确定集合中的元素是否有 2 的倍数,再使用 forall 来确定集合的所有元素是否都是 2 的倍数:
let intArray = [|0; 1; 2; 3; 4; 5; 6; 7; 8;9|]
let existsMultipleOfTwo =
intArray |>
Seq.exists (fun x -> x % 2 = 0)
let allMultipleOfTwo =
intArray |>
Seq.forall (fun x -> x % 2 = 0)
printfn "existsMultipleOfTwo: %b"existsMultipleOfTwo
printfn "allMultipleOfTwo: %b"allMultipleOfTwo
代码的运行结果如下:
existsMultipleOfTwo: true
allMultipleOfTwo: false
filter、find 和 tryFind 函数
下面的示例看的三个函数与exists 和 forall 有些相似;这些函数分别是filter,其类型为type (‘a -> bool) -> #seq<‘a> -> seq<‘a>,find,其类型为 (‘a-> bool) -> #seq<‘a> -> ‘a,和 tryfind,其类型为 type(‘a -> bool) -> #seq<‘a> -> ‘a。它们之所以与 exists 和 forall 相似,是因为它们都是用函数来确定集合的内容;但这些函数并不返回布尔值,而是返回实际找到的值。函数filter 用传递给它的函数测试集合中的每一项;函数filter 然后返回一个列表,包含满足这个函数条件的所有元素;如果没有元素满足条件,返回空列表。find 和 tryFind 函数则返回集合中满足条件的第一个元素,条件由传递进来的函数决定;当集合中没有满足条件的元素时,其行为有所改变。如果没有找到元素,find 会引发异常;tryFind 则返回可选类型(option)None。因为 .NET 中的异常代价高昂,所以,最好用tryFind 代替 find。
在下面的示例中,将检查一个单词列表,首选用 filter 创建一个列表,只包含以 at 结尾的单词;再用 find 查找第一个以 ot 结尾的单词;最后,用tryfind 单词中是否有以 tt 结尾的单词。
let shortWordList = [|"hat";"hot"; "bat"; "lot"; "mat";"dot"; "rat";|]
let atWords =
shortWordList
|>Seq.filter (fun x -> x.EndsWith("at"))
let otWord =
shortWordList
|>Seq.find (fun x -> x.EndsWith("ot"))
let ttWord =
shortWordList
|>Seq.tryFind (fun x -> x.EndsWith("tt"))
atWords |> Seq.iter (fun x -> printf"%s ... " x)
printfn ""
printfn "%s" otWord
printfn "%s" (match ttWord with |Some x -> x | None -> "Not found")
代码的运行结果:
hat ... bat ... mat ... rat ...
hot
Not found
choose 函数
下一个要看的序列函数是一个聪明的函数,它可以同时完成筛选(filter)并映射(map),这个函数称为 choose(选择),其类型为(‘a -> ‘b option) -> #seq<‘a> -> seq<‘b>。要使用这个函数,传递给choose 的函数必须是返回选项(option)类型。如果列表中的元素可能转换成有用的内容,函数就会返回 Some,包含这个新值;如果没有希望的值,函数返回 None。
在下面的示例中,我们把一个浮点数列表乘以 2,如果是整数,就返回;否则,就滤掉。这样,剩下的列表就是整数了。
let floatArray = [|0.5; 0.75; 1.0; 1.25;1.5; 1.75; 2.0 |]
let integers =
floatArray |>
Seq.choose
(fun x ->
let y = x * 2.0
let z = floor y
if y - z = 0.0 then
Some (int z)
else
None)
integers |> Seq.iter (fun x -> printf"%i ... " x)
代码的运行结果如下:
1 ... 2 ... 3 ... 4 ...
init 和 initInfinite 函数
接下来,我们看两个初始化集合的函数:init,其类型为:int -> (int -> ‘a) -> seq<‘a>,initInfinite,其类型为:(int-> ‘a) -> seq<‘a>。可以用 init [ 这里可能是笔误,原文为initInfinite ] 函数来创建一个有限大小的集合;调用这个函数,传递给它次数,以及传递对这一次的说明。可以使用initInfinite 函数创建无限大小的集合;调用这个函数,传递给它的参数是每一次创建新元素的方法。在理论上,可以创建无限大小的列表,但是,实际中是受机器执行计算的限制。
下面的示例演示了用 init 来创建有 10 个整数的列表,每一个值都是 1;还演示了创建可以包含所有可能的 32 位整数的列表,并用 take 函数取列表的前 10 项。
let tenOnes = Seq.init 10 (fun _ -> 1)
let allIntegers = Seq.initInfinite (fun x-> System.Int32.MinValue + x)
let firstTenInts = Seq.take 10 allIntegers
tenOnes |> Seq.iter (fun x -> printf"%i ... " x)
printfn ""
printfn "%A" firstTenInts
代码的运行结果如下:
1 ... 1 ... 1 ... 1 ... 1 ... 1 ... 1 ... 1... 1 ... 1 ...
[-2147483648; -2147483647; -2147483646;-2147483645; -2147483644; -2147483643;
-2147483642; -2147483641; -2147483640;-2147483639]
unfole 函数
我们已经在第三章中见到过 unfold 函数,它比函数 init 和 initInfinite 更加灵活。unfold 的第一个好处是可以整数计算的过程中传递一个累加器,这样,就能够保存计算过程吕的一些状态,而不必只依靠在列表中的当前位置来计算值,如在init 和 initInfinite 中所做的;第二个好处,产生的列表既可以是有限的,也可以是无限的。这两个好处的实现,是通过把的返回类型传递给 unfold。函数的返回类型是‘a * ‘b option,表示一个可选(option)类型,包含一个元组值。在这个可选类型中的第一个值是将要放在列表中的值;第二个值是累加累加器。如果想让列表持续下去,放返回 Some,在其中包含这个元组;如果想停止,就返回 None。
下面的示例是第三章中的重复,演示了用 unfold 计算斐波那契(Fibonacci)数。可以看到,累加器用来保存元组,其值表示在斐波那契数列中的下两个数。因为斐波那契数的列表是无限的,因此,根本不需要返回 None。
let fibs =
(1,1)|> Seq.unfold
(fun(n0, n1) ->
Some(n0, (n1, n0 + n1)))
let first20 = Seq.take 20 fibs
printfn "%A" first20
代码运行的结果如下:
[1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89; 144;233; 377; 610; 987;
1597; 2584; 4181; 6765]
下面的示例演示了用 unfold 来产生一个可终止的列表,假设我们想计算一个数列,其值是当前值的一半,就如原子核的衰变;假设超过某一特定值,数字变得很小,我们就可以忽略它了。这样的序列是通过当值达到限定时,返回 None 来建模的:
let decayPattern =
Seq.unfold
(funx ->
let limit = 0.01
let n = x - (x / 2.0)
if n > limit then
Some(x, n)
else
None)
10.0
decayPattern |> Seq.iter (fun x ->printf "%f ... " x)
代码的运行结果如下:
10.000000 ... 5.000000 ... 2.500000 ...1.250000 ...
0.625000 ... 0.312500 ... 0.156250 ...0.078125 ... 0.039063 ...
generate 函数
generate 函数的类型为 (unit ->‘b) -> (‘b -> ‘a option) -> (‘b -> unit) -> seq<‘a>,可用于创建可枚举(IEnumerable)的集合,可以从一些各类的游标(cursor)创建集合,比如文件系统,或者数据库的记录集。游标可以是文件系统,如下面的示例所演示的,也可以更为常见的数据库游标。事实上,它可以是能够生成序列元素的任何类型。generate 函数有三个函数参数:一个是打开的游标(下面示例中的 opener 函数),另一个是做实际的产生集合的工作(generator 函数),还有一个是关闭游标(closer 函数)。那么,这个集合就可以被当作任何可枚举集合看待,但是,在后台,定义的这些函数会被调用,去打开数据源,并从中读取元素。下面的示例演示的函数从文件中读取以逗号分隔的词语列表:
open System
open System.Text
open System.IO
// test.txt: the,cat,sat,on,the,mat
let opener() = File.OpenText("test.txt")
let generator (stream : StreamReader) =
letendStream = ref false
letrec generatorInner chars =
matchstream.Read() with
|-1 ->
endStream := true
chars
|x ->
match Convert.ToChar(x) with
| ‘,‘ -> chars
| c -> generatorInner (c :: chars)
letchars = generatorInner []
ifList.length chars = 0 && !endStream then
None
else
Some(newstring(List.toArray (List.rev chars)))
let closer (stream : StreamReader) =
stream.Dispose()
let wordList =
Seq.generate
opener
generator
closer
wordList |> Seq.iter (fun s ->printfn "%s" s)
代码的运行结果如下:
the
cat
sat
on
the
mat
cast 函数
.NET 框架的 BCL 中包含两个版本的可枚举(IEnumerable)接口,一个定义在 System.Collections.Generic,另一个老一点的定义在 System.Collections。到此炎止,所有设计的示例都是用来处理来自 System.Collections.Generic 的新版本。然而,有时也可能需要处理非泛型集合,这样,F# 的IEnumerable 模块也提供了一个函数,用来处理从非泛型集合到泛型的转换。
在使用这个函数之前,我强烈建议你看一下,是否可以使用在第三、四章中讨论的列表推导(list comprehension)语法,这是因为列表推导可以许多非类型化集合的类型,通常是通过看 Item 索引属性的类型,因此,很少需要类型注解(type annotations),通常使编程更容易。
出于某种原因,如果宁可不使用列表推导语法,而使用 cast 函数把非泛型集合转换成体泛型也是可以的,下面的示例就是:
open System.Collections
open System.Collections.Generic
let floatArrayList =
lettemp = new ArrayList()
temp.AddRange([|1.0; 2.0; 3.0 |])
temp
let (typedFloatSeq: seq<float>) =Seq.cast floatArrayList
使用 cast 函数就必须使用类型注解,告诉编译器我们产生的列表是什么类型。这里我们的列表是浮点型,因此,使用类型注解seq<float>,告诉编译器这是一个包含浮点数的可枚举集合。
原文地址:http://blog.csdn.net/hadstj/article/details/24726375