Scala之Partially Applied Function和Currying

Partially Applied Function

在开始之前,还是让我们说说”apply”这个术语怎么解释吧。在scala里,我们认为:把一个参数传递给函数的过程就叫apply, 在传统的编程语言里,传递参数和调用函数是一个过程,并不存在apply这一说,但是在像scala这样的函数式编程语言里,“apply”和“invoke”这两个动作是可以分离开的,所以才有了Partially Apply这样一种动作。简单地说呢,就是我们可以给一个函数的一个或几个参数设定好值(就是部分的apply),但先不调用它,因为另外某些参数的值我们还不确定,进过这种操作之后,实际上你得到的已经是一个新的函数了(编译器自动帮你搞定),在调用它时,你只需要传递那些还没有赋值的参数就可以了。看这样一个例子吧:

scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int,b: Int,c: Int)Int

scala> val a = sum _
a: (Int, Int, Int) => Int = <function3>

scala> a(1, 2, 3)
res11: Int = 6 

让我们解释一下这个示例中发生的事情:

首先明确两点:

  1. sum是一个方法,不是一个函数!你要搞清楚方法和函数的区别。
  2. paritally apply这个动作发生在“sum _”上。

在编译器编译val a = sum _这部分时,一个函数的实例(或者就函数对象,标准的叫法是函数值,这里用“实例”可能更加形象)由编译器创建了,那么这个函数实例的类型是(Int, Int, Int) => Int或者说是Function3[Int,Int,Int,Int], 伴随这个函数对象一起生成的当然是一个apply方法,我们可以认为这个apply方法的参数列表就是sum方法的参数列表,它的内部实现是仅仅是调用了一下sum方法,为了印证这一点,你可以使用a.apply(1, 2, 3)去试试一下。所以从效果上看,sum _告诉编译器:给我包装一下sum方法吧,提升(lifting)成一个函数对象,这样我就可以把它当成一个变量随意地赋值和传递了,也就是这里的a.

回到partially apply这个话题上,显然针对val a = sum _这个例子而言,它是一个paritally applied function的特例,特殊在它实际上是”fully applied”, 那么更为常见的是像下面这样的例子。

一个paritally applied function的例子

加入你需要一个函数来生产HTML片段,所有的标签都有一个前缀,一个后缀,和内容,所以这个方法应该是这样的:

def wrap(prefix: String, html: String, suffix: String) = {
prefix + html + suffix
}

但是针对大量重复的标签,你每次去生成HTML片段时都需要重复的传递相同的前缀和后缀,这个时候是应用paritally applied function一个理想的场景。拿常见的<div>xxxx</div>来说吧,你可以通过paritally apply”创建”一个新的“函数”,这个函数由wrap方法演变而来,并专门正对div标签的HTML片段工作:

val wrapWithDiv = wrap("<div>", _: String, "</div>")

然后你可以只向wrapWithDiv传递内容片顿即可:

scala> wrapWithDiv("<p>Hello, world</p>")
res0: String = <div><p>Hello, world</p></div>

在这个例子中,wrap方法的前缀和后缀参数传入了固定的值,但是内容参数传递是下划线,这意味着,你要告诉编译器对wrap方法进行一次“提升”了,首先编译还是先生成了一个函数对象,并自动生成了apply方法,这个apply方法只有一个参数,也就是html片段的内容部分,然后apply的方法体内还是简单地调用了wrap方法,前缀与后缀参数已经封装在函数对象内了,再加上传入的内容参数,三个参数传给warp就完成了调用。

Currying

柯里化是这样一种技术:它让一个函数具有了多个参数列表(是多个参数”列表”哦,也就是函数名后面可以跟多对小括号),而不再是一个参数列表。以下是一个例子:

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (x: Int,y: Int)Int
scala> plainOldSum(1, 2)
res4: Int = 3 

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (x: Int)(y: Int)Int
scala> curriedSum(1)(2)
res5: Int = 3 

plainOldSum是旧式的求和函数,curriedSum是柯里化后的版本。你会看到,curriedSum函数是有两个参数列表的,也就是两对小括号。当你以curriedSum(1)(2)的形式去调用这个函数时实际上发生的动作是:curriedSum接受第一个参数1之后,会返回一个函数,这个返回的函数再来接受传入的第二个参数2,这是柯里化的运作方式。但是这并不意味着你可以这样去调用:

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (x: Int)(y: Int)Int

scala> val first=curriedSum(1)
<console>:8: error: missing arguments for method curriedSum;
follow this method with `_' if you want to treat it as a partially applied function val first=curriedSum(1) ^

你看到了,编译器报错了,因为前面我们说的是一个柯里化函数的后台运作方式,从调用的角度来看,curriedSum毕竟是需要两个参数集合的,如果你只给定一个,编译器自然会报错,除非你显式地告诉编译器:我想要得到“接受了第一个参数之后转化而来的那个函数”,具体的做法就是在curriedSum(1)加一个下划线。

scala> val first=curriedSum(1)_
first: Int => Int = <function1>

上面的动作就是做了一个partially apply,下划线就是转化之后的函数的参数列表的占位符。但是这里有一个有意思的地方,按理说我们应该这样写:val first=curriedSum(1) _也就是在函数名后面加一个空格再跟下滑线,就像val p=println _一样,但是你不能写成val p=println_,因为println_永远是一个合法的标示符,因而发生了歧义,但是curriedSum(1)_则不会有歧义,所以是可以这样写的。但总的来说统一使用空格再加下划线更直观易读。

柯里化技术的使用场景是把一个通用性或者讲叫泛化的函数向一个特定的方向去转化,进而得到一个需要的函数。

Most of time, we actually use in the other way by creating a specialized version of generalized function through curry function.

以前面的例子为例,我们可以创建这样的函数:

scala> val plusOne = curriedSum(1) _
plusOne: Int => Int = <function1>

scala> plusOne(2)
res0: Int = 3

scala> val plusTwo = curriedSum(2) _
plusTwo: Int => Int = <function1>

scala> plusTwo(2)
res1: Int = 4

显然plusOne这个函数是curriedSum固定了第一参数的值之后而得到的,它的用途也和这个值绑定在一起了,同样的,对于plusTwo 也就是一样。

这里我们只是再通过改变参数的值的展示柯里化技术,更加丰富和生动的示例应该是发生在高阶函数里。

最后提一下,Scala提供了相应的机制可以让函数在柯里化和非柯里化之间自由的转换:

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (x: Int, y: Int)Int
scala> val curriedSum = (plainOldSum _).curried
curriedSum: Int => (Int => Int) = <function1>
scala> curriedSum(1)(2)
res2: Int = 3
scala> val uncurriedSum = Function.uncurried(curriedSum)
uncurriedSum: (Int, Int) => Int = <function2>
scala> uncurriedSum(1,2)
res4: Int = 3

两种技术的对比

只从最终实再的效果上来看,我认为两种技术是非常相似的,都是通过对函数参数进行部分应用(partially apply )将函数转换成了另一个函数。从灵活性上对比的话,显然Partially Applied Function更加灵活,因为它不需要像柯里化那样,必须针对参数列表集从左至右链式地进行转换。

来源: http://write.blog.csdn.net/mdeditor#!postId=51038414

你可能感兴趣的:(Applied,currying,柯里化,partially,部分应用函数)