一旦你完全掌握了函数运用,就可以开始把他们结合形成更大、更强的函数。这就是所谓的函数组成,也是函数编程的另一个宗旨。
在我们介绍函数组成功能之前,先让我们看看利用传统的编程方式,是怎么来解决问题的,下面的示例演示了统计一个指定的路径下的所有文件大小:
let sizeOfFolder folder =
// Get all files under the path
let filesInFolder : string [] =
Directory.GetFiles(
folder, "*.*",
SearchOption.AllDirectories)
// Map those files to their corresponding FileInfo object
let fileInfos : FileInfo [] =
Array.map
(fun file -> new FileInfo(file))
filesInFolder
// Map those fileInfo objects to the file's size
let fileSizes : int64 [] =
Array.map
(fun (info : FileInfo) -> info.Length)
fileInfos
// Total the file sizes
let totalSize = Array.sum fileSizes
// Return the total size of the files
totalSize
这里主要有三个问题在上面的代码中:
1、类型推理系统无法自动确定正确的类型,所以我们必须在每个lambda表达式的参数提供类型注释。
2、每个计算的结果仅仅是作为下一个函数的一个参数。
3、这要花费程序员更多的时间去读一行一行代码解读是怎么回事。
函数组成就是这样的,它把很大的一个函数分解成更小的代码段,然后组合起来,最终得到一个计算结果。上面的演示代码就是一直从顶到下,一直保持着计算,并且按照顺序,把计算结果送给下一个函数。在数学上,如果我们要将f(x)的结果传递到g(x),我们可以这样写g(f(x))。这样一来,我们就可以避免通过let绑定而通过嵌套所有的中间结果,比如下面的代码:
let uglySizeOfFolder folder =
Array.sum
(Array.map
(fun (info : FileInfo) -> info.Length)
(Array.map
(fun file -> new FileInfo(file))
(Directory.GetFiles(
folder, "*.*",
SearchOption.AllDirectories))))
但是,虽然这样比起前一个代码量少多了,然后由于层层嵌套,如果只是嵌套一两层还可以看得懂,要是一个多层嵌套,就难免会让人感到不解了,特别是F#中对空格和缩进有着非常严格的规则,一不小心就会因为一个缩进导致编译不通过。幸运的是,在F#中提供了另外一种方便的解决途径,对于函数的组合,F#提供了四个操作符|>、<|、<<、>>。下面来一一介绍如何使用:
1、|>
对于上面的示例,把每步结算结果暂时放入到一个中间值中,然后再把中间值传给下一个函数的做法,F#提供了一个更简洁的操作|>。|>的定义如下:
let (|>) x f = f x
|>操作符允许你重新排列一个函数的参数以便第一个参数变成函数的最后一个参数。例如List.iter的最后一个参数是一个列表,我们可以通过|>操作符来重新排列参数,把参数放置到最前面:
> [1 .. 3] |> List.iter (printfn "%d");;
1
2
3
val it : unit = ()
|>操作符的好处就是,可以不断的重复使用参数给函数,如果有多个函数组成一个函数链,则可以把每个函数的结果传递给下一个函数。回顾下开头我们的那个例子,根据给定的目录获取目录下文件的大小,通过|>操作符,我们就可以省略中间值了:
let sizeOfFolderPiped folder =
let getFiles folder =
Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
let totalSize =
folder
|> getFiles
|> Array.map (fun file -> new FileInfo(file))
|> Array.map (fun info -> info.Length)
|> Array.sum
totalSize
这样一来,比如函数的组合嵌套,代码就更加的简洁,短小了。另外一个好处就是|>操作符还可以帮助类型推理系统来进行类型推断。您不能访问任何值的属性或方法,如果编译器不知道它的类型是什么。因此,必须专门使用类型注释来明确类型,比如下面的代码段就不能推断出一个值是否就有lenght属性:
> List.iter (fun s -> printfn "s has length %d" s.Length) ["Pipe"; "Forward"];;
List.iter (fun s -> printfn "s has length %d" s.Length) ["Pipe"; "Forward"];;
----------------------------------------------^^^^^^^^
stdin(1,47): error FS0072: Lookup on object of indeterminate type based on infor
mation prior to this program point. A type annotation may be needed prior to thi
s program point to constrain the type of the object. This may allow the lookup t
o be resolved.
由于|>操作符可以让编译器更早的知道最后一个参数的类型,所以类型推理系统能够确定一个函数的正确类型,从而消除了必要的注释类型。所以,在下面的代码中,由于使用了|>操作符,编译器就知道了参数的类型为string:
> ["Pipe"; "Forward"] |> List.iter (fun s -> printfn "s has length %d" s.Length);;
s has length 4
s has length 7
val it : unit = ()
2、>>运算
>>操作把连个函数组合一起,并且左边的第一个函数被调用。它的类型是:
let (>>) f g x = g(f x)
当你使用|>操作符时,你在最前面还需要一个占位符变量。如在我们上面介绍的示例中,还是需要利用folder参数来直接传递到第一个函数中。然后,利用>>操作符,就不需要参数来做一个占位符了,我们只需要简单的把各个函数组合起来利用>>:
let sizeOfFolderComposed =
let getFiles folder =
Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
getFiles
>> Array.map (fun file -> new FileInfo(file))
>> Array.map (fun info -> info.Length)
>> Array.sum;;
组成一个简单的函数简单的例子也可以这样:
> let square x = x * x
- let toString (x : int) = x.ToString()
- let strLen (x : string) = x.Length
- let lenOfSquare = square >> toString >> strLen;;
val square : int -> int
val toString : int -> string
val strLen : string -> int
val lenOfSquare : (int -> int)
> square 128;;
val it : int = 16384
> lenOfSquare 128;;
val it : int = 5
3、<|运算符
<|运算符就是在其左边接受一个函数,其右边就是一个参数,它的类型是:
let (<|) f x = f x
我们可以通过一个简单的列子来说明如何运用:
> List.iter (printfn "%d") <| [1..3];;
1
2
3
val it : unit = ()
这样一看,可能会觉得这跟List.iter (printfn "%d") [1..3]没有什么区别。其实,<|远不止这么的简单,它允许你改变运算的优先权,或者改变函数的执行顺序。在一个函数中,参数是从左向右的,这意味着如果你要调用一个函数,结果传递给另一个函数,您有两种选择:用括号把优先执行的表达式括起来或者使用<|运算符,例如:
> printfn "The result of sprintf is %s" (sprintf "(%d, %d)" 1 2);;
The result of sprintf is (1, 2)
val it : unit = ()
或
> printfn "The result of sprintf is %s" <| sprintf "(%d, %d)" 1 2;;
The result of sprintf is (1, 2)
val it : unit = ()
4、<<运算符
<<运算符接受两个函数,并且首先执行右边的函数,然后在把结果传递给左边的函数执行。它一般用于当你你想表达相反顺序的想法时,它的类型如下:
let (<<) f g x = f(g x)
下面的代码演示了如何把一个数的平方转换成它的负数,利用<<操作符允许程序文本的阅读与函数操作方式相同,换一句话说,让程序代码阅读起来实际上更像在操作它:
> let square x = x * x
- let negate x = -x;;
val square : int -> int
val negate : int -> int
> (square >> negate) 10;;
val it : int = -100
> (*但是我们真正需要的是求一个负数的平方,那么我们可以利用<<操作符*)
- (square << negate) 10;;
val it : int = 100
另外一个简单的例子就是利用List.filter来过滤列表中的空列表元素:
> [ [1]; []; [4;5;6]; [3;4]; []; []; []; [9] ]
- |> List.filter(not << List.isEmpty);;
val it : int list list = [[1]; [4; 5; 6]; [3; 4]; [9]]