前几天和王福强讨论一个偏函数的问题,聊到了auto tupling
的问题,以前有过几篇blog是跟这个相关的。参考:scala雾中风景(16): println(1,2,3)为什么work?,scala雾中风景(17): toSet()的谜题;
其实当方法参数是Unit类型时,传入的参数不是Unit的话编译器最后自动生成一个”()” 也属于auto tupling
的机制,只是之前不知道它们是同一种机制造成的,参考:scala雾中风景(4): Unit类型, 和 scala雾中风景(8): 高阶函数与Unit的谜题
不过在跟afo讨论的过程中发现另一个现象,用下面的例子来看:
class A{
val p: Function[(Int,Int),String] = { case (a:Int,b:Int) => "OK" }
val p2: Function2[Int,Int,String] = { case (a:Int,b:Int) => "OK" }
}
这里面的偏函数{ case (a:Int,b:Int) => "OK" }
即可以声明为Function
类型,也可以声明为Function2
类型。
最初我以为这里编译器是把偏函数类型向上造型,第一个能通过,第二个通不过,因为PartialFunction
类型是继承自A=>B
这种只有一个参数的函数类型,即Function[A,B]
,见scala源码:
trait PartialFunction[-A, +B] extends (A => B)
所以,偏函数对象可以造型为Function
,不能造型为Function2
,因为不是它的子类;但实际中这么写却能编译通过,也没有给出任何警告,觉得有些蹊跷,确认了一下后边的偏函数{ case (x,y) ... }
里面case后边的小括号在模式匹配中是被当成Tuple
对待的,猜测背后是类型推断时期做了一些适配的事情。
scala> import scala.tools.reflect.ToolBox
scala> val tb = scala.reflect.runtime.currentMirror.mkToolBox()
scala> tb.parse("val p2: Function[(Int,Int),String] = { case (a:Int,b:Int) => \"OK\" }")
res0: tb.u.Tree =
val p2: Function[scala.Tuple2[Int, Int], String] = <empty> match {
case scala.Tuple2((a @ (_: Int)), (b @ (_: Int))) => "OK"
}
scala> tb.parse("val p2: Function2[Int,Int,String] = { case (a:Int,b:Int) => \"OK\" }")
res1: tb.u.Tree =
val p2: Function2[Int, Int, String] = <empty> match {
case scala.Tuple2((a @ (_: Int)), (b @ (_: Int))) => "OK"
}
今天有空用typer-debug
参数分析了一下过程, 发现在类型推断的时候,也存在把一个tuple类型适配为(adapt)若干个方法参数的过程;这个过程跟auto tupling
相反,称为auto detupling
;通过编译器的-Ytyper-debug
选项,分别编译这两种写法:
class A{
val p: Function[(Int,Int),String] = { case (a:Int,b:Int) => "OK" }
}
$ scala-2.11.4/bin/scalac -Yno-adapted-args -Ytyper-debug A.scala >a 2>&1
和
class A{
val p: Function2[Int,Int,String] = { case (a:Int,b:Int) => "OK" }
}
$ scala-2.11.4/bin/scalac -Yno-adapted-args -Ytyper-debug B.scala >b 2>&1
然后通过diff工具来比较:
可以看到左边红色是Function2
的声明,高亮部分:Tuple2
确实被适配为了2个方法参数:
[adapt] Tuple2.type adapted to [T1, T2](_1: T1, _2: T2)(T1, T2)
遗憾的是,不像auto tupling
可以通过-Yno-adapted-args
编译选项禁止,auto detupling
没有参数可以禁止掉,或许有我没有找到。
val p1: Function[(Int, Int), String] = {
x => "OK"
}
val p3: Function[(Int, Int), String] = {
x =>
x match {
case (a: Int, b: Int) => "OK"
}
}
val p3: Function[(Int, Int), String] = {
case (a: Int, b: Int) => "OK"
}
先写以上三种 Function[(Int, Int), String] 的写法。
注意,p1 是所有 Function[T, U] 的标准写法,尽管在这里 T 是 (Int, Int),但对于编译器来说,最简单的处理方案就是统一按 p1 对待。
然后看 p2,这个写法也是编译器很容易理解和处理的,没什么 magic。
再然后,在 Scala 中,下列形式的代码:
{
x =>
x match {
case xxxxxx =>
}
}
总是可以缩写为
{
case xxxxxx =>
}
这样,就自然得到了 p3,这也是编译器的一个统一约定,编译器理解和实现也是很一致和简单的。
其实在 Map[T, U] 的 foreach 和偏函数等初中常看到这种缩写方式:
Map("a" -> 1, "b" -> 2).forach {
entry => entry match {
case (k, v) =>
}
}
总可以写成:
Map("a" -> 1, "b" -> 2).forach {
case (k, v) =>
}
多谢草原的解释。
上面第二个函数命名打错了,应该是 p2
不过博主谈的其实应该是调用时为何 p(1,2) 和 p((1,2)) 都行 :-)
也即,若scalac -Yno-adapted-args,则 p(1, 2) 编译不过,只有 p((1, 2)) 可。
function2,function3这种用法更正常一点,这是内置的traits