函数结合性和组成
从已有的函数中构造函数
函数结合性
如果在一行中有一个函数链,他们应该如何组合?
例如,这是什么意思?
let F x y z = x y z
它是不是意思是z作为函数y的参数,之后用y的结果作为函数x参数?
在这个例子中,像这样:
let F x y z = x (y z)
或者意思是y作为函数x的参数,之后z作为函数x的返回函数的参数?在这个例子中,像这样:
let F x y z = (x y)
答案是后者。函数应用是左联的。就是,执行x y z和执行(x y) z是一样的。还有执行 w x y z和执行((w x) y) z是一样的。这不奇怪。我们已经知道分部应用是如何工作的了。如果你想象x是一个两个参数的函数,第一个参数的分部应用的结果就是(x y) z,接着传递一个参数z到这个中间函数中。
如果你想用右联,你可以用括号指明,或者你也可以用管道。这三种格式都是相同的:
let F x y z = x (y z) let F x y z = y z |> x // using forward pipe let F x y z = x <| y z // using backward pipe
作为一个测试,不执行它们的情况下计算出来它们的签名。
函数组合
我们之前已经很多次提到函数组合,但是它实际上是什么意思呢?它一开始看起来很吓人,但实际上很简单。
已知你有一个从类型"T1"映射到类型"T2"的函数"f"而且你有一个从类型"T2"映射到类型"T3"的函数"g"。之后你可以新建一个从类型"T1"映射到类型"T3"函数把"f"的输出和"g"的输入联系起来。
这有一个例子:
let f (x:int) = float x * 3.0 // f is int->float let g (x:float) = x > 4.0 // g is float->bool
我们可以新建一个拿"f"的输出作为"g"的输入的函数。
let h (x:int) = let y = f(x) g(y) // return output of g
更加紧凑的写法是:
let h (x:int) = g ( f(x) ) // h is int->bool //test h 1 h 2
到目前为止,如此简单。我们可以定一个叫做"组合"的新函数,有趣的是给定函数"f"和"g",用一个甚至不知道他们签名的方式就可以组合他们。
let compose f g x = g ( f(x) )
如果你执行它,你可以看到编译器会正确的推导出"f"是一个从泛型‘a到泛型‘b的函数,而且函数"g"被限制为输入类型是‘b。全部的签名是这样的:
val compose : (‘a -> ‘b) -> (‘b -> ‘c) -> ‘a -> ‘c
(注意泛型组合操作只是有可能的因为函数只能有一个输入和一个输出。这种方法在非函数式语言中是不可行的。)
正如我们所看到的,组合的实际定义是使用“>>”符号。
let (>>) f g x = g ( f(x) )
根据这个定义,我们可以从已经存在的函数中构建一个新函数。
let add1 x = x + 1 let times2 x = x * 2 let add1Times2 x = (>>) add1 times2 x //test add1Times2 3
这种直接的方式很杂乱。我们可以做一些事使它更便于使用和理解。
首先,我们抛开参数"x"以至于组合操作符返回一个分部应用。
let add1Times2 = (>>) add1 times2
现在我们有一个两个东西组成的操作,所以我们可以把操作符放在中间。
let add1Times2 = add1 >> times2
就是这样。使用组合操作符使代码更加干净也更加直观。
let add1 x = x + 1 let times2 x = x * 2 //old style let add1Times2 x = times2(add1 x) //new style let add1Times2 = add1 >> times2
实践组合操作符
组合操作符(像所有的中缀操作符)优先级低于正常的函数应用。这意味着组合函数中的函数不需要用括号。
例如,如果"add"和"times"函数有一个附加的参数,它可以在组合期间被传递。
let add n x = x + n let times n x = x * n let add1Times2 = add 1 >> times 2 let add5Times3 = add 5 >> times 3 //test add5Times3 1
只要输入和输出匹配,任何一种值都可以参与到函数。例如,考虑下边的,执行一个函数两次:
let twice f = f >> f //signature is (‘a -> ‘a) -> (‘a -> ‘a)
注意编译器推断出输入和输出必须都是同样的类型。
现在考虑像"+"这样的函数。像我们之前看到的,输入是个int,但是输出实际上是一个分部应用函数int->int。"+"的输出也可以是"twice"的输入。所以我们可以这样写:
let add1 = (+) 1 // signature is (int -> int) let add1Twice = twice add1 // signature is also (int -> int) //test add1Twice 9
另一面方面,我们不能这样写:
let addThenMultiply = (+) >> (*)
因为"*"的输入必须是int值,不是一个int->int函数(加法的输出是)。
但是如果调整它以至于它的输出而是一个int值,之后它就可以运行:
let add1ThenMultiply = (+) 1 >> (*) // (+) 1 has signature (int -> int) and output is an ‘int‘ //test add1ThenMultiply 2 7
如果需要,组合也可以使用"<<"向后这样做。
let times2Add1 = add 1 << times 2 times2Add1 3
反转组合可以使代码看起来更像英语那样。例如,这个简单的例子:
let myList = [] myList |> List.isEmpty |> not // straight pipeline myList |> (not << List.isEmpty) // using reverse composition
组合对比管道
在这一节,你可能惊讶组合操作符和管道操作符的不同,尽管它们看起来很相似。
首先,我们再看一下管道操作符的定义:
let (|>) x f = f x
所有这些允许我们把函数的参数放在前边而不是后边。就是这样。如果函数有多个参数,输入将会是最后一个参数。这是我们之前用到的例子:
let doSomething x y z = x+y+z doSomething 1 2 3 // all parameters after function 3 |> doSomething 1 2 // last parameter piped in
组合不是同样的东西也不会是管道的代替品。在下边的例子中数字3甚至不是一个函数,所以它的"输出"不会进入"doSomething":
3 >> doSomething 1 2 // not allowed // f >> g is the same as g(f(x)) so rewriting it we have: doSomething 1 2 ( 3(x) ) // implies 3 should be a function! // error FS0001: This expression was expected to have type ‘a->‘b // but here has type int
编译器抱怨"3"应该是‘a->‘b的顺序。
对比组合的定义,它需要三个参数,前两个必须是函数。
let (>>) f g x = g ( f(x) ) let add n x = x + n let times n x = x * n let add1Times2 = add 1 >> times 2
尝试使用管道也不可行。在下边的例子中,"add1"是一个类型是int->int的(分部)函数,不能用作"times 2"的第二个参数。
let add1Times2 = add 1 |> times 2 // not allowed // x |> f is the same as f(x) so rewriting it we have: let add1Times2 = times 2 (add 1) // add1 should be an int // error FS0001: Type mismatch. ‘int -> int‘ does not match ‘int‘
编译器抱怨"times 2"应该用一个int->int类型的参数,而不是,(int->int)->‘a类型。
翻译有误,请指正,谢谢!
原文地址:http://fsharpforfunandprofit.com/posts/function-composition/
翻译目录传送门:http://www.cnblogs.com/JayWist/p/5837982.html