标签:
带入函数中的一些参数
在上一篇关于柯里化的文章中,我们了解它将多参数函数分为较小的一个参数函数。这是数学上一种正确的方式,但是不是它可以运行的唯一原因-它也导致出现一个叫做分部函数应用(partial function application)的非常强大的技术。在函数式编程中,被宽泛的使用,理解它很重要。
分部应用的思想是,当你解决第一个N参数问题时,你得到剩余参数的函数。从柯里化的讨论中,你可能知道它出现的很自然。
下面是证明它的一些简单例子:
// create an "adder" by partial application of add let add42 = (+) 42 // partial application add42 1 add42 3 // create a new list by applying the add42 function // to each element [1;2;3] |> List.map add42 // create a "tester" by partial application of "less than" let twoIsLessThan = (<) 2 // partial application twoIsLessThan 1 twoIsLessThan 3 // filter each element with the twoIsLessThan function [1;2;3] |> List.filter twoIsLessThan // create a "printer" by partial application of printfn let printer = printfn "printing param=%i" // loop over each element and call the printer function [1;2;3] |> List.iter printer
在每个例子中,我们都创建一个在多个上下文中重用的分部应用函数。
当然,分部应用函数也可以只涉及到修复函数参数的问题。看这些例子:
// an example using List.map let add1 = (+) 1 let add1ToEach = List.map add1 // fix the "add1" function // test add1ToEach [1;2;3;4] // an example using List.filter let filterEvens = List.filter (fun i -> i%2 = 0) // fix the filter function // test filterEvens [1;2;3;4]
下面的更复杂的示例演示了如何使用相同的方法来创建透明的“插件”行为。
// create an adder that supports a pluggable logging function let adderWithPluggableLogger logger x y = logger "x" x logger "y" y let result = x + y logger "x+y" result result // create a logging function that writes to the console let consoleLogger argName argValue = printfn "%s=%A" argName argValue //create an adder with the console logger partially applied let addWithConsoleLogger = adderWithPluggableLogger consoleLogger addWithConsoleLogger 1 2 addWithConsoleLogger 42 99 // create a logging function that creates popup windows let popupLogger argName argValue = let message = sprintf "%s=%A" argName argValue System.Windows.Forms.MessageBox.Show( text=message,caption="Logger") |> ignore //create an adder with the popup logger partially applied let addWithPopupLogger = adderWithPluggableLogger popupLogger addWithPopupLogger 1 2 addWithPopupLogger 42 99
融入日志的函数可以像其它函数一样被依次调用。例如,我们可以为添加42新建一个分部应用,之后把他传入列表函数,就像我们为简单函数"add42"做的那样。
// create a another adder with 42 baked in let add42WithConsoleLogger = addWithConsoleLogger 42 [1;2;3] |> List.map add42WithConsoleLogger [1;2;3] |> List.map add42 //compare without logger
分部应用函数是一个很有用的工具。我们可以灵活的(但不混乱的)创建库函数,还使创建可复用的默认值变得简单,以至于调用者不必一直暴漏复杂的事物。
为分部应用设计函数
你知道参数的排序可以使分部应用在易用性上变得很大差异。例如,List库中的大多数函数像List.map和List.filter都有相同的格式,像这样:
列表总是最后一个参数。这是一些完整格式的例子:
同样的例子使用分部应用:
如果库中函数的参数被写成不同的顺序,用分部应用去使用它们将会很不方便。
例如你写你自己的多参数函数,你会想到最好的参数排序是什么。像所有设计性问题一样,这个问题没有"正确"的答案,但是这有一些普遍被接受的准则。
规则1很直接。最有可能用分部应用"固定"的参数必须放在第一位。我们早在日志的例子中看到过。
规则2使从函数传送结构或者集合到另一个函数变得简单。我们已经在列表函数中看到很多次了。
// piping using list functions let result = [1..10] |> List.map (fun i -> i+1) |> List.filter (fun i -> i>5)
同样,分部应用列表函数,易于构成,因为列表参数本身可以省略。
let compositeOp = List.map (fun i -> i+1) >> List.filter (fun i -> i>5) let result = compositeOp [1..10]
在F#中,很容易去使用.NET基类库,但是它不是为了像F#这样的函数式编程语言使用才设计出来的。例如,大多数函数的数据参数应该在第一位,但在F#中,我们都知道,正常情况下数据参数应该在最后一个。
然而,很容易的去创建更符合语言习惯的封装。例如,在下边的代码段中,.net字符串函数被改写成包含字符串对象的是最后一个参数而不是第一个参数。
// create wrappers for .NET string functions let replace oldStr newStr (s:string) = s.Replace(oldValue=oldStr, newValue=newStr) let startsWith lookFor (s:string) = s.StartsWith(lookFor)
一旦字符串参数成为最后一个参数,我们可以用期望的方式在管道中使用它们:
let result = "hello" |> replace "h" "j" |> startsWith "j" ["the"; "quick"; "brown"; "fox"] |> List.filter (startsWith "f")
或者在函数组合中:
let compositeOp = replace "h" "j" >> startsWith "j" let result = compositeOp "hello"
现在你已经知道分部应用是如何工作的,你应该有能力去理解"管道"函数是怎么工作的。
管道函数像这样定义:
let (|>) x f = f x
它的作用是允许你把参数放在函数的前边而不是后边。就这样。
let doSomething x y z = x+y+z doSomething 1 2 3 // all parameters after function
如果函数有多个参数,那么输入将是最后一个参数。实际发生的是,函数部分应用,返回一个函数只有一个参数:输入。
这是一些被分部应用重写的例子
let doSomething x y = let intermediateFn z = x+y+z intermediateFn // return intermediateFn let doSomethingPartial = doSomething 1 2 doSomethingPartial 3 // only one parameter after function now 3 |> doSomethingPartial // same as above - last parameter piped in
像你看到的这样,在F#中管道运算符极为常见,一直自然流动的使用。它有更多的用法,你可能会看到:
"12" |> int // parses string "12" to an int 1 |> (+) 2 |> (*) 3 // chain of arithmetic
你可能看到颠倒的管道函数"<|"被使用。
let (<|) f x = f x
和正常的不同,它看起来什么都不会做,为什么它会存在?
原因是,当用中缀形式作为二进制操作,它减少了对括号的需要,使代码更干净。
printf "%i" 1+2 // error printf "%i" (1+2) // using parens printf "%i" <| 1+2 // using reverse pipe
你可以一起使用正反两个管道运算符做一个伪中缀操作。
let add x y = x + y (1+2) add (3+4) // error 1+2 |> add <| 3+4 // pseudo infix
翻译有误,请指正,谢谢。
原文地址:http://fsharpforfunandprofit.com/posts/partial-application
翻译目录传送门:http://www.cnblogs.com/JayWist/p/5837982.html
标签:
原文地址:http://www.cnblogs.com/JayWist/p/5860511.html