Scala的偏应用函数
尽管前面的例子里下划线替代的只是单个参数,你还可以使用一个下划线替换整个参数列表。例如,写成println(_),或者更好的方法你还可以写成println _。下面是一个例子:
someNumbers.foreach(println _)
51CTO编辑推荐:Scala编程语言专题
Scala把这种短格式直接看作是你输入了下列代码:
someNumbers.foreach(x => println(x))因此,这个例子中的下划线不是单个参数的占位符。它是整个参数列表的占位符。请记住要在函数名和下划线之间留一个空格,因为不这样做编译器会认为你是在说明一个不同的符号,比方说是,似乎不存在的名为println_的方法。
以这种方式使用下划线时,你就正在写一个偏应用函数:partially applied function。Scala里,当你调用函数,传入任何需要的参数,你就是在把函数应用到参数上。如,给定下列函数:
scala> def sum(a: Int, b: Int, c: Int) = a + b + c sum: (Int,Int,Int)Int你就可以把函数sum应用到参数1,2和3上,如下:
scala> sum(1, 2, 3) res12: Int = 6偏应用函数是一种表达式,你不需要提供函数需要的所有参数。代之以仅提供部分,或不提供所需参数。比如,要创建不提供任何三个所需参数的调用sum的偏应用表达式,只要在“sum”之后放一个下划线即可。然后可以把得到的函数存入变量。举例如下:
scala> val a = sum _ a: (Int, Int, Int) => Int = < function>有了这个代码,Scala编译器以偏应用函数表达式,sum _,实例化一个带三个缺失整数参数的函数值,并把这个新的函数值的索引赋给变量a。当你把这个新函数值应用于三个参数之上时,它就转回头调用sum,并传入这三个参数:
scala> a(1, 2, 3) res13: Int = 6实际发生的事情是这样的:名为a的变量指向一个函数值对象。这个函数值是由Scala编译器依照偏应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法带三个参数。产生的类扩展了特质Function3,定义了三个参数的apply方法。之所以带三个参数是因为sum _表达式缺少的参数数量为三。Scala编译器把表达式a(1,2,3)翻译成对函数值的apply方法的调用,传入三个参数1,2,3。因此a(1,2,3)是下列代码的短格式:
scala> a.apply(1, 2, 3) res14: Int = 6Scala编译器根据表达式sum _自动产生的类里的apply方法,简单地把这三个缺失的参数前转到sum,并返回结果。本例中apply调用了sum(1,2,3),并返回sum返回的,6。
这种一个下划线代表全部参数列表的表达式的另一种用途,就是把它当作转换def为函数值的方式。例如,如果你有一个本地函数,如sum(a: Int, b: Int, c: Int): Int,你可以把它“包装”在apply方法具有同样的参数列表和结果类型的函数值中。当你把这个函数值应用到某些参数上时,它依次把sum应用到同样的参数,并返回结果。尽管不能把方法或嵌套函数赋值给变量,或当作参数传递给其它方法,但是如果你把方法或嵌套函数通过在名称后面加一个下划线的方式包装在函数值中,就可以做到了。
现在,尽管sum _确实是一个偏应用函数,或许对你来说为什么这么称呼并不是很明显。这个名字源自于函数未被应用于它所有的参数。在sum _的例子里,它没有应用于任何参数。不过还可以通过提供某些但不是全部需要的参数表达一个偏应用函数。举例如下:
scala> val b = sum(1, _: Int, 3) b: (Int) => Int = < function>这个例子里,你提供了第一个和最后一个参数给sum,但中间参数缺失。因为仅有一个参数缺失,Scala编译器会产生一个新的函数类,其apply方法带一个参数。在使用一个参数调用的时候,这个产生的函数的apply方法调用sum,传入1,传递给函数的参数,还有3。如下:
scala> b(2) res15: Int = 6这个例子里,b.apply调用了sum(1,2,3)。
scala> b(5) res16: Int = 9这个例子里,b.apply调用了sum(1,5,3)。
如果你正在写一个省略所有参数的偏应用程序表达式,如println _或sum _,而且在代码的那个地方正需要一个函数,你可以去掉下划线从而表达得更简明。例如,代之以打印输出someNumbers里的每一个数字(定义在第113页)的这种写法:
someNumbers.foreach(println _)你可以只是写成:
someNumbers.foreach(println)最后一种格式仅在需要写函数的地方,如例子中的foreach调用,才能使用。编译器知道这种情况需要一个函数,因为foreach需要一个函数作为参数传入。在不需要函数的情况下,尝试使用这种格式将引发一个编译错误。举例如下:
scala> val c = sum < console>:5: error: missing arguments for method sum... follow this method with `_' if you want to treat it as a partially applied function val c = sum ˆ scala> val d = sum _ d: (Int, Int, Int) => Int = < function> scala> d(10, 20, 30) res17: Int = 60为什么要使用尾下划线?
Scala的偏应用函数语法凸显了Scala与经典函数式语言如Haskell或ML之间,设计折中的差异。在经典函数式语言中,偏应用函数被当作普通的例子。更进一步,这些语言拥有非常严格的静态类型系统能够暴露出你在偏应用中可能犯的所有错误。Scala与指令式语言如Java关系近得多,在这些语言中没有应用所有参数的方法会被认为是错误的。进一步说,子类型推断的面向对象的传统和全局的根类型接受一些被经典函数式语言认为是错误的程序。
举例来说,如果你误以为List的drop(n: Int)方法如tail(),那么你会忘记你需要传递给drop一个数字。你或许会写,“println(drop)”。如果Scala采用偏应用函数在哪儿都OK的经典函数式传统,这个代码就将通过类型检查。然而,你会惊奇地发现这个println语句打印的输出将总是< function>!可能发生的事情是表达式drop将被看作是函数对象。因为println可以带任何类型对象,这个代码可以编译通过,但产生出乎意料的结果。