首页 > 代码库 > 分部应用

分部应用

分部应用

带入函数中的一些参数


在上一篇关于柯里化的文章中,我们了解它将多参数函数分为较小的一个参数函数。这是数学上一种正确的方式,但是不是它可以运行的唯一原因-它也导致出现一个叫做分部函数应用(partial function application)的非常强大的技术。在函数式编程中,被宽泛的使用,理解它很重要。

分部应用的思想是,当你解决第一个N参数问题时,你得到剩余参数的函数。从柯里化的讨论中,你可能知道它出现的很自然。

下面是证明它的一些简单例子:

// create an "adder" by partial application of addlet add42 = (+) 42    // partial applicationadd42 1add42 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 applicationtwoIsLessThan 1twoIsLessThan 3// filter each element with the twoIsLessThan function[1;2;3] |> List.filter twoIsLessThan // create a "printer" by partial application of printfnlet printer = printfn "printing param=%i" // loop over each element and call the printer function[1;2;3] |> List.iter printer   

 

在每个例子中,我们都创建一个在多个上下文中重用的分部应用函数。

当然,分部应用函数也可以只涉及到修复函数参数的问题。看这些例子:

// an example using List.maplet add1 = (+) 1let add1ToEach = List.map add1   // fix the "add1" function// testadd1ToEach [1;2;3;4]// an example using List.filterlet filterEvens =    List.filter (fun i -> i%2 = 0) // fix the filter function// testfilterEvens [1;2;3;4]

 

下面的更复杂的示例演示了如何使用相同的方法来创建透明的“插件”行为。

  • 我们新建一个两个数字相加的函数,但是在相加的时候用一个记录数字和结果的日志函数。
  • 日志函数有两个参数,(string)"名称"和(generic)"值",所以它有"string->‘a->unit"签名。
  • 之后我们可以创建多种日志函数的实现,例如控制台记录器或者一个弹出记录器。
  • 最后新建一个带特殊日志的函数融入到分部应用函数中。
// create an adder that supports a pluggable logging functionlet 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 consolelet consoleLogger argName argValue =http://www.mamicode.com/     printfn "%s=%A" argName argValue //create an adder with the console logger partially appliedlet addWithConsoleLogger = adderWithPluggableLogger consoleLogger addWithConsoleLogger  1 2 addWithConsoleLogger  42 99// create a logging function that creates popup windowslet popupLogger argName argValue =http://www.mamicode.com/     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 appliedlet addWithPopupLogger  = adderWithPluggableLogger popupLogger addWithPopupLogger  1 2 addWithPopupLogger  42 99

 

融入日志的函数可以像其它函数一样被依次调用。例如,我们可以为添加42新建一个分部应用,之后把他传入列表函数,就像我们为简单函数"add42"做的那样。

// create a another adder with 42 baked inlet add42WithConsoleLogger = addWithConsoleLogger 42 [1;2;3] |> List.map add42WithConsoleLogger  [1;2;3] |> List.map add42               //compare without logger 

 

分部应用函数是一个很有用的工具。我们可以灵活的(但不混乱的)创建库函数,还使创建可复用的默认值变得简单,以至于调用者不必一直暴漏复杂的事物。

为分部应用设计函数

你知道参数的排序可以使分部应用在易用性上变得很大差异。例如,List库中的大多数函数像List.mapList.filter都有相同的格式,像这样:

列表总是最后一个参数。这是一些完整格式的例子:

同样的例子使用分部应用:

如果库中函数的参数被写成不同的顺序,用分部应用去使用它们将会很不方便。

例如你写你自己的多参数函数,你会想到最好的参数排序是什么。像所有设计性问题一样,这个问题没有"正确"的答案,但是这有一些普遍被接受的准则。

  •  放在前边的:参数更多像是静态的
  •  放在最后的:数据结构或者集合(或者最不同的参数)
  • 对于众所周知的操作如"subtract",放在预期的位置

规则1很直接。最有可能用分部应用"固定"的参数必须放在第一位。我们早在日志的例子中看到过。

规则2使从函数传送结构或者集合到另一个函数变得简单。我们已经在列表函数中看到很多次了。

// piping using list functionslet 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]

 

为分部应用封装基类库(BCL-Basic Class Library)函数

在F#中,很容易去使用.NET基类库,但是它不是为了像F#这样的函数式编程语言使用才设计出来的。例如,大多数函数的数据参数应该在第一位,但在F#中,我们都知道,正常情况下数据参数应该在最后一个。

然而,很容易的去创建更符合语言习惯的封装。例如,在下边的代码段中,.net字符串函数被改写成包含字符串对象的是最后一个参数而不是第一个参数。

// create wrappers for .NET string functionslet replace oldStr newStr (s:string) =   s.Replace(oldValue=oldStr, newValue=http://www.mamicode.com/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+zdoSomething 1 2 3       // all parameters after function

 

如果函数有多个参数,那么输入将是最后一个参数。实际发生的是,函数部分应用,返回一个函数只有一个参数:输入。

这是一些被分部应用重写的例子

let doSomething x y  =    let intermediateFn z = x+y+z   intermediateFn        // return intermediateFnlet doSomethingPartial = doSomething 1 2doSomethingPartial 3     // only one parameter after function now3 |> doSomethingPartial  // same as above - last parameter piped in

 

像你看到的这样,在F#中管道运算符极为常见,一直自然流动的使用。它有更多的用法,你可能会看到:

"12" |> int               // parses string "12" to an int1 |> (+) 2 |> (*) 3       // chain of arithmetic

 

颠倒的管道函数

你可能看到颠倒的管道函数"<|"被使用。

let (<|) f x = f x

 

和正常的不同,它看起来什么都不会做,为什么它会存在?

原因是,当用中缀形式作为二进制操作,它减少了对括号的需要,使代码更干净。

printf "%i" 1+2          // errorprintf "%i" (1+2)        // using parensprintf "%i" <| 1+2       // using reverse pipe

 

你可以一起使用正反两个管道运算符做一个伪中缀操作。

let add x y = x + y(1+2) add (3+4)          // error1+2 |> add <| 3+4        // pseudo infix

 

 

 


翻译有误,请指正,谢谢。

原文地址:http://fsharpforfunandprofit.com/posts/partial-application

翻译目录传送门:http://www.cnblogs.com/JayWist/p/5837982.html

 

分部应用