scala雾中风景(5): 中缀表达

scala中的中缀表达在3个地方存在:操作符、模式匹配、类型声明

1)中缀操作符最常见: a + bx foo y 里的+foo都是中缀操作符(严格的说scala里没有操作符,其实都是方法)。

其中需要注意的一点是名称最后以“:”结尾的,调用顺序与普通中缀操作符想反:从右往左。比如h :: t 实际是 t.::(h),它的意义在于可以让表达看起来更“顺眼”一些。比如我们自定义一个:

// 一个Cache单例,提供了 >>: 方法
scala> object Cache { def >>:(data:String) { println(data) } }
defined module Cache

scala> "myData" >>: Cache
myData

上面的方法很直观,让人一看就容易理解是把数据追加到cache中。还可以支持连续操作,稍修改一下:

scala> object Cache { def >>:(data:String):Cache.type = { println(data); Cache } }
defined module Cache

scala> "data2" >>: "data1" >>: Cache
data1
data2
res6: Cache.type = Cache$@45d2d12f

实际中,最常见的从右往左结合的中缀操作符是 List:: 方法

scala> val list = List()
list: List[Nothing] = List()

scala> "A" :: "B" :: list
res4: List[String] = List(A, B)

2) 当一个高阶类型有2个参数化类型,比如

scala> class Foo[A,B]

在声明变量类型时,也可以用 A Foo B 中缀形式来表达,Foo也称为中缀类型

scala> val x: Int Foo String = null

Int Foo String 等同于 Foo[Int,String]

3) 包含两个参数的构造器,在模式匹配时也可以用中缀表达

scala> case class Cons(a:String,b:String)
defined class Cons

scala> val x = Cons("one", "two")
x: Cons = Cons(one,two)

scala> x match { case "one" Cons "two" => println("ok") }
ok

"one" Cons "two" 等同于 Cons("one", "two")

对于2)和3)似乎也体现了某种一致性,不过暂对中缀类型的好处没有体会,对于类型系统中的这种风格也还不了解,暂不做比较。

现实中有个很容易迷惑人的地方,常用的 List ,即存在了一个:: 方法, 也存在了一个:: 名字的case类(List的子类)。

所以,构造一个list可以通过::的伴生对象提供的工厂方法,也可以通过Nil的::方法

scala> val l = ::("A",Nil) // 这里::是伴生对象, 相当于 ::.apply()
l: scala.collection.immutable.::[String] = List(A)

scala> val l2 = "A" :: Nil // 这里::是方法, 相当于 Nil.::()
l2: List[String] = List(A)

scala> l == l2
res1: Boolean = true

在模式匹配时,可以用中缀形式:

// 这里::是伴生对象,相当于 ::.unapply()
case head :: tail => …

所以::在不同的场景,意思不同,有时是方法名,有时是伴生对象名。这个设计,最初了解时很爱吐槽。但它提供了一种看上去很一致的表象,在中缀形式下都是表示”头”与”尾”的连接。

scala雾中风景(4): Unit类型

这个例子是一次错误的尝试时发现的,通常我们不太会用Unit类型声明参数,更多情况它是出现在函数类型声明里的出参部分(如 String=>Unit)。这个例子仅用作案例分析,先考虑下面的方法执行结果会如何?

scala> def foo(p:Unit) = {println("hi")}
foo: (p: Unit)Unit

scala> foo(2)

直觉上,感觉编译会报错,类型不匹配么,声明Unit类型,传入的确实Int类型。

实际上尝试一下会发现,它编译和运行都很正常。甚至无所谓你传入几个参数,或什么类型:

scala> foo(2,3,"OK")
hi

都能正常运行,看起来太诡异了。这背后隐含了编译器对Unit类型变量在赋值时的处理逻辑。

Unit类型是值类型,全局只存在唯一的值:()

scala> val a:Unit = ()
a: Unit = ()

若尝试把其它类型的值赋给它,也ok:

scala> val a:Unit = 2
a: Unit = ()    // 暂忽略编译器的警告,跟我们要关注的无关

诊断一下:

$ scala -nocompdaemon -Xprint:typer  -e 'val a:Unit = 2'
… 
private[this] val a: Unit = {
    2;
    ()  // 编译器自动增加了一个Unit的值:()
};
…

原因是编译对待等号右边的表达式,看待成为“行为”,会把这段“行为”统一放入花括号里。
并判断最后一句表达式的值是不是Unit类型的,如果不是则自动增加一个值:()

所以等号右边的 2 最终被转化为 { 2; () } 所以 foo(2,3,"OK") 也是被翻译为了

foo( {(2, 3, "OK"); ()} )

参数封装在一个tuple里,在最后返回一个 ()

再探究一步,Unit类型通常是用于声明方法返回值的,比如:

def foo:Unit = 2

它被翻译为:

def foo: Unit = {
    2;
    () // 编译器判断结果返回不是Unit类型的话,自动在最后返回()
}

当Unit用在值的类型时,编译器保持与方法一致的处理逻辑。

小结:通常Unit只用来声明函数或方法的返回值,其他场景基本是没有意义的。

scala雾中风景(3): for表达式的背后

这个例子是以前从scala-user邮件列表看到的,我借用这里例子加工了一下,这是一个for表达式转换的细节

//有一个map,里面有一些自定义类型的数据,然后在用下面的for进行操作
for((key,value) <- m; name=key) { 
    println(name) 
}

他在运行时发现上面的代码时发现key的hashCode会被调用,而正好这个hashCode的实现很复杂,导致效率很低。而他使用下面的写法则没有问题:

for((key,value) <- m) { 
    val name=key
    println(name) 
}

分析一下第一种写法为什么会导致hashCode方法被执行,先模拟一下:

scala> class MyKey { override def hashCode() = { println("key"); super.hashCode }}

scala> class MyValue { override def hashCode() = { println("value"); super.hashCode }}

// 把上面的k,v放入一个map;在放入Map的过程 k和v的hashCode方法都被执行了
scala> val m = Map[MyKey, MyValue](new MyKey() -> new MyValue())
key     
value   

// 模拟第一种for表达式
scala> for( (k,v) <- m; name = k ) { println(name+" do nothing") }
key     // k的hashCode被调用
$line3.$read$$iw$$iw$MyKey@63b97fd0 do nothing

是否这里又把k放入了Map?在上面的for表达式里,没有用到yield,是否直接被转换为foreach呢?

for表达式的展开和转换不在这里讨论,关于其语法背后的转换可以参考这篇文章。我们聚焦在这个案例的细节上。for表达式可以这样描述:

for([pattern <- generator; definition*]+; filter* )
    [yield] expression

在for小括号(也可以用花括号)内部由一个或多个 p <- generator; definition* 以及0个或多个filter组成。后跟着一个可选的yield关键字,以及后续的逻辑表达。

p <- generator; definition* 这句再展开,由一个 p <- generator 以及0个或多个definition组成。

这个问题的细节就在于有没有definition对for表达式在转换时的影响;也就是说 for(p <- generator)for(p <- generator; definition) 是不一样的。

没有yield关键字,for((key,value) <- m) 直接翻译为

m.foreach(…)

for((key,value) <- m; name=key) 被翻译为

m.map(…).foreach(…)

为什么中间多了一次map操作?我们通过一个简单例子来看:

scala> import reflect.runtime.universe._

scala> val list = List("A","B","C")

scala> reify( for(e <- list; x=e;y=e) { println(x+y) } )
res1: reflect.runtime.universe.Expr[Unit] =
Expr[Unit]($read.list.map(((e) => {
    val x = e;
    val y = e;
    Tuple3.apply(e, x, y)
}))(List.canBuildFrom).foreach(((x$1) => x$1: @unchecked match {
    case Tuple3((e @ _), (x @ _), (y @ _)) => Predef.println(x.$plus(y))
})))

从reify的结果可以看到,对于for(e <- list; x=e; y=e) 里面的x=ey=e两个definition中的变量x,y与e一同用一个tuple封装起来,即map的结果得到的是一个 List[Tuple3] 类型的数据。随后再对这个新结果进行foreach

所以最初的问题是因为for里面存着了一个definition,而导致转换过程多了一次map,也就是创建了一个新的Map[Tuple2],并把数据放入新Map,在这个过程导致了hashCode的调用。

小结:对于for表达式要知道背后会被转换为什么,不要滥用for的语法糖,误用导致低效率。

scala雾中风景(2): 小括号与花括号

下面的问题,表面上看是小括号与花括号的问题。

// map方法这样写不能编译通过
scala> List(2).map( case 2 => "OK" )

// 换做花括号就可以了
scala> List(2).map{ case 2 => "OK" }

不了解原因的话,觉得很诡异。分析一下,首先,map方法接受一个函数,这个函数将List中的元素映射为其他类型。

实际上case 2 => "OK" 不是一段lambda表达式(也就是说它不是函数),它是一段模式匹配语句。
那为什么在第二行可以编译通过呢?

稍微有点基础的话,会清楚方法的花括号有2种意思:
1)scala中函数的小括号,可以用花括号来表示,即foo{xx}foo(xx)是一回事儿。
2)对于只有一个参数的方法,其小括号是可以省略的,map(lambda)可写为 map lambda,即这块{case 2 => "OK"} 连同花括号整体是一个lambda(函数字面量)。

这儿显然是第2个(追究原因就要看编译器在语法解析式的优先级了,看样子把花括号对待为lambda字面量的一部分要高于把花括号当作小括号来对待),那么为什么加了花括号的{case 2 => "OK" }就可以当作一段函数字面量?

这要引出偏函数的概念,所谓偏函数(也叫部分函数)与完全函数想对应,普通的方法都是完全函数,即 f(i:Int) = xxx 是将所有Int类型作为参数的,是对整个Int集的映射;而偏函数则是对部分数据的映射,比如上面{case 2=> "OK" }就仅仅只对2做了映射。偏函数的实现都是通过模式匹配来表达的。

scala> val p:PartialFunction[Int,String] = { case 2 => "OK" }

因为偏函数是通过 { case x => y } 这种特殊的方式来描述的,上面的{case 2=>"OK"}就被当作了一段偏函数字面量,而偏函数背后的类型PartialFunction[A,B]是继承自Function1[A,B]的,所以将这段匿名的偏函数传给map方法是ok的。

小结:表达式 {case x=>y}会被当作偏函数字面量。