聚合操作
尽管list提供了一些方法来把数据组合在一起,但是没有什么特别,真正充满魔力的还是list的聚合操作,这个一个非常强大的功能,对于任何的集合来说,下面将介绍几个聚合函数。
1、List.map
List.map是根据提供的一个函数来创建一个新的list,新list的中每个元素都是根据提供的函数产生的值创建的,它的类型是:
('a -> 'b) -> 'a list -> 'b list
下面的代码显示了通过squares函数来把结果映射到一个整数列表:
> let squares x = x * x;;
val squares : int -> int
> List.map squares [1..10];;
val it : int list = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]
虽然看起来很简单,但是在F#语言中是最有用的函数之一,它为你的数据转换提供了一个优雅的方式让您的数据转换和反复使用时可以简化编写的代码结构。
2、List.fold
List.fold在聚合操作中是最强大、有用的类型,当然也是很复杂的。当你有一个列表,想把里面的数据转换成单一的实体是,就可以使用List.fold,就像在C#中的string.join()方法样,把一个数组转换成一个逗号分隔的字符串,但是List.fold的功能比这强大的多了。在List.fold中主要有两种类型可以使用,第一种是List.reduce,它的类型签名是:
('a -> 'a -> 'a) -> 'a list -> 'a
List.reduce遍历列表的每一个元素,建立一个累加器值,把在列表中处理完成的数据汇集起来,一旦每个列表元素已处理,最后累加器返回值,在List.reduce中累加器的初始值是列表的第一个元素。下面的代码演示了如何使用List.reduce以逗号分开字符串列表,函数insertCommas利用累加器和一个值,然后简单的返回一个用逗号分隔的新字符串。当传递给List.reduce,累加器的初始值是列表中的第一个项目,所以当列表中的每一个元素处理后最终结果是包含列表中所有元素的并且用逗号分号的一个单一字符串。
> let insertCommas (acc : string) item = acc + "," + item;;
val insertCommas : string -> string -> string
> insertCommas ["Jack";"Jill";"Jim";"Joe";"Jane"];;
val it : string = "Jack,Jill,Jim,Joe,Jane"
下面的列表显示了累加器是如果构建字符串的当处理完每一个列表元素后:
Accumulator | List element |
“Jack” (the first list element) | “Jill” (the second list element) |
“Jack, Jill” | “Jim” |
“Jack, Jill, Jim” | “Joe” |
“Jack, Jill, Jim, Joe” | “Jane” |
“Jack, Jill, Jim, Joe ,Jane ” | [] |
上面的列子中List.reduce迫使累加器的类型要跟列表的元素类型相同,但是如果你想要更强大的功能,例如,减少了在购物车中的物品清单后计算金额,该怎么做?
如果你想使用自定义累加器,你可以使用List.fold,第一个参数是一个函数,提供了一个累加器和一个列表,用来返回新的累加器;二,初步累加器值;最后一个参数是列表。该函数的返回值是累加器的最终值,下面是它的类型:
('acc -> 'b -> 'acc) -> 'acc -> 'b list -> 'acc
下面提供了一个简单的列子,演示了利用List.fold来计算列表元素的总和:
> let addAccToListItem acc i = acc + i;;
val addAccToListItem : int -> int -> int
> List.fold addAccToListItem 0 [1; 2; 3];;
val it : int = 6
但是同样,累加器的类型还是需要跟列元素的类型一致,下面就用另外一个示例来说明可以自定义一个累加器类型。
下面的代码演示了在一个字符串中统计没一个元音字母出现的次数,当折叠函数应用到列表中的每一个字母是, 如果一个元音,我们返回一个更新的累加器值,否则,我们就返回现有累加器:
> let countVowels(str : string) =
- let charList = List.ofSeq str
- let accFun(As,Es,Is,Os,Us) letter =
- if letter = 'a' then (As+1,Es,Is,Os,Us)
- elif letter = 'e' then (As,Es+1,Is,Os,Us)
- elif letter = 'i' then (As,Es,Is+1,Os,Us)
- elif letter = 'o' then (As,Es,Is,Os+1,Us)
- elif letter = 'u' then (As,Es,Is,Os,Us+1)
- else (As,Es,Is,Os,Us)
- List.fold accFun (0,0,0,0,0) charList;;
val countVowels : string -> int * int * int * int * int
> countVowels "The quick brown fox jumps over the lazy dog";;
val it : int * int * int * int * int = (1, 3, 1, 4, 2)
3、List.iter
最后的一个聚合函数List.iter,作用是遍历列表的每个元素和调用一个函数,作为一个参数传递,它类型是:
('a -> unit) -> 'a list -> unit
从返回的类型unit,我们知道,调用List.iter将不会返回值,例如下面的代码:
> let printNumber x = printfn "Printing %d" x
- List.iter printNumber [1..5];;
Printing 1
Printing 2
Printing 3
Printing 4
Printing 5
val printNumber : int -> unit
Option
如果你想表示一个值,可能或可能不存在,最好的方法这样做是使用option类型,这option类型仅仅只有两种可能值:Some('a) 和None。思考一下从字符串转换成整数,如果字符串格式正确,则返回对应的整数值,但如果字符串格式不正确?在这种情况下,你将使用一个option类型来判断。
下面的实例中定义了一个isInteger函数,试图转换一个字符串到整数值,如果转换成功,它将返回Some(Result),否则返回None。这就使让调用该函数的人知道,输入的值不能被转换,因而返回None。
> open System
- let isInteger str =
- let successful,result = Int32.TryParse(str)
- if successful then
- Some(result)
- else
- None;;
val isInteger : string -> int option
> isInteger "this is not an int";;
val it : int option = None
> isInteger "100";;
val it : int option = Some 100
在C#中,一个常见的情况是利用null来判断类型是否转换成功,然而,nul也可以用来表示未初始化一个值,这种双重性可能导致混乱和错误,如果使用option类型,毫无疑问值代表什么。
要检索一个option的值,可以使用Option.get,如果在None中调用Option.get,将会抛出一个异常。下面的代码片断定义了一个函数
包含NegativeNumbers,它利用Some(_)返回一些在列表中的所有负数。然后,该Option.get来检索返回的负数列表:
> let isLessThanZero x = (x < 0)
- let containsNegativeNumbers intList =
- let filteredList = List.filter isLessThanZero intList
- if List.length filteredList > 0 then
- Some(filteredList)
- else
- None;;
val isLessThanZero : int -> bool
val containsNegativeNumbers : int list -> int list option
> let negativeNumbers = containsNegativeNumbers [6; 20; -8; 45; -5];;
val negativeNumbers : int list option = Some [-8; -5]
> Option.get negativeNumbers;;
val it : int list = [-8; -5]
在Option模块包含的其他有用的功能一个是Option.isSome,判断如果option是Some,则返回true,否则返回false;还有一个是Option.isNone,作用跟Option.isSome一样
Printfn
把数据输出到控制台最简单的办法就是利用Printf的系列函数,printf有三个表现形式:printf,printfn,和sprintf。printf接受输入并将其输出到屏幕上, 而printfn输出到屏幕上,并另起一行,如下面代码:
> printf "hello"
- printf "wordl";;
hellowordl>
- ;;
> printf "hello"
- printfn "wordl";;
hellowordl
>
打印文本到控制台并不特别的兴奋, 但printf增加了很多有用的功能在于它可以格式化并检查输入,如像下面的代码段:
let mountain = "K2"
let height = 8611
let units = 'm';;
val mountain : string = "K2"
val height : int = 8611
val units : char = 'm'
> printfn "%s is %d%c high" mountain height units;;
K2 is 28251m high
val it : unit = ()
最重要的是,当使用F#的类型推断系统,编译器就会给你一个错误,如果数据不匹配给定的格式说明符:
> printfn "An integer = %d" 1.23;;
printfn "An integer = %d" 1.23;;
--------------------------^^^^^
stdin(2,27): error FS0001: The type 'float' is not compatible with any of the
types byte,int16,int32,int64,sbyte,uint16,uint32,uint64,nativeint,unativeint,
arising from the use of a printf-style format string.
stopped due to error
此外,由于F#编译器知道什么类型的期望给了格式列表符,类型推断系统能够牵制这些值的类型,例如,在下列代码中,该函数的参数的类型是根据上下文来进行推断的:
let inferParams x y z =
printfn "x = %f, y = %s, z = %b" x y z;;
val inferParams : float -> string -> bool -> unit
当你要把一个字符串作为打印结果是可以使用sprintf:
> let location = "World";;
val location : string
> sprintf "Hello, %s" location;;
val it : string = "Hello, World"
下面列出了printf的格式
Hole | Description | Example | Result |
%d, %i | Print any integer | printf "%d" 5 | 5 |
%x, %o | Print any integer in Hex or Octal format | printfn "%x" 255 | ff |
%s | Print any string | printf "%s" "ABC" | ABC |
%f | Print any floating-point number | printf "%f" 1.1M | 1.100000 |
%c | Print any character | p rintf"%c" '/097' | a |
%b | Print any Boolean | printf "%b" false | false |
%O | Print any object | printfn "%O" (1,2) | (1, 2) |
%A | Print anything | printf "%A" (1, []) | (1, []) |