标签归档:type-system

scala雾中风景(17): toSet()的谜题

看到hackernews上推荐的《scala: the good,the bad and the very ugly》在这篇ppt里,有一个例子挺有意思的:

List(1,2,3).toSet()

猜一下它的结果,直观上我们会认为它返回一个Set类型的集合,实际却不是:

scala> List(1,2,3).toSet()
<console>:8: warning: Adaptation of argument list by inserting () has been deprecated: this is unlikely to be what you want.
    signature: GenSetLike.apply(elem: A): Boolean
  given arguments: <none>
 after adaptation: GenSetLike((): Unit)
          List(1,2,3).toSet()
                           ^
res1: Boolean = false

在repl下测试,在给出警告之后,输出了一个Boolean值。让人大跌眼镜,要想得到预期的结果,方法后边的小括号是不能写的:

scala> List(1,2,3).toSet
res9: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

其实警告信息里已经很明确的说明了,toSet()实际是对toSet的结果再调用的GenSetLike.apply(elem: A),即它相当于

List(1,2,3).toSet.apply()

如果apply方法是无参的,上面的也好理解,但警告信息里提示的是:GenSetLike.apply(elem: A) 明明时带有一个参数的,为什么用空的参数apply()也可以运行?

等等,警告信息里还有2句:

given arguments: <none>
after adaptation: GenSetLike((): Unit)

这两句是说给了一个空参数,被编译器适配成了一个Unit类型的()实例对象,是不是有些匪夷所思?编译器为何要自作聪明?如果我们对Unit类型的谜题还有印象的话,会怀疑是否因为Unit类型用作方法参数引起的:

scala> def foo(u:Unit) { println("ok") }

scala> foo() // 可以运行,同样也会有类似上面的警告信息

参考以前两篇有关Unit的: scala雾中风景(4): Unit类型scala雾中风景(8): 高阶函数与Unit的谜题

不过,这里是否真的就是Unit类型引起的? 虽然警告信息里提示把空参数适配成了Unit类型,但并不是apply方法声明的参数,GenSetLike.apply(elem: A) 参数类型是一个泛型参数,我们先根据直觉判断 List(1,2,3).toSet 之后得到的是Set[Int],所以这里A是Int类型?我们一步步来验证一下:

scala> val s = List(1,2,3).toSet
s: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> s.apply() // 错误,提示缺乏参数!用IDE的话发现编译时就提示错误了

诡异了,分成两步的时候居然又不行了,虽然这时的提示更符合预期,但之前用一句表达 List(1,2,3).toSet.apply() 为什么又可以运行?

经过google之后,发现这竟然是一个很有趣的类型推导问题,参考这篇讨论

scala.collection.immutable.Set是非协变的(实际是invariant的),这点不同于List,Seq,Queue等主流集合的声明。为什么要定义为非协变的,也是由Set自身的函数特征决定的trait Set[A] extends (A => Boolean),可以参考这里的解释

TraversableOnce.toSet方法声明是:

def toSet[B >: A]: immutable.Set[B] = ...

注意,它返回的类型不同于List[A]里的A,而是A的某个父类型B!这里存在一个很常见的类型推导问题:

scala> List(1,2,3).toSet.map(x=>x+1) // 错误,缺乏参数类型

scala> List(1,2,3).toSet.map((x:Int)=>x+1) // 需要显式声明参数类型 
res28: scala.collection.immutable.Set[Int] = Set(2, 3, 4)

scala> List(1,2,3).toSet[Int].map(x=>x+1) // 或在toSet的时候显式的声明元素类型为Int 
res29: scala.collection.immutable.Set[Int] = Set(2, 3, 4)

或分两步,先得到set结果,再map不需要显式声明参数类型:

scala> val s = List(1,2,3).toSet // 先得到set结果
s: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> s.map(x=>x+1)  // OK
res30: scala.collection.immutable.Set[Int] = Set(2, 3, 4)

为啥分开写,跟连着写在toSet阶段的类型推导是不一样的呢,typesafe的开发Adriaan Moors给了解释

You can think of inference as a breadth-first sweep over the expression, collecting constraints (which arise from subtype bounds and required implicit arguments) on type variables, followed by solving those constraints. This approach allows, e.g., implicits to guide type inference. In your example, even though there is a single solution if you only look at the xs.toSet subexpression, later chained calls could introduce constraints that make the system unsatisfiable. The downside of leaving the type variables unsolved is that type inference for closures requires the target type to be known, and will thus fail (it needs something concrete to go on — the required type of the closure and the type of its argument types must not both be unknown).

Now, when delaying solving the constraints makes inference fail, we could backtrack, solve all the type variables, and retry, but this is tricky to implement (and probably quite inefficient).

大致是类型推导是广度优先的方式扫描表达式,搜集类型变量上由子类型界定和隐式参数导致的约束,接下来解释这些约束。。。

所以在一整句连着写List(1,2,3).toSet.apply()toSet阶段因为其上下文约束不同于单句的List(1,2,3).toSet,还未能推导此时Set的元素类型。

我们通过-Ytyper-debug选项可以看到分开写的时候,类型推导的确是明确推导出结果是Set[Int]了:

而连在一起写的时候,并未明确推导出Set的元素类型,这里B类型是Int的父类型:

现在,我们了解到了List(1,2,3).toSeq.apply()在apply()之前因为元素类型还未明确(B是Int的父类型),B可能是一个非常“泛”的类型,比如AnyVal或Any,这样相当于:

scala> val s: Set[Any] = List(1,2,3).toSeq
scala> s.apply()  // 给出警告,但运行OK!

现在问题就变成了,为和一个很宽泛的类型比如AnyAnyRef,用作参数类型的时候,实际调用这个方法时参数可以随意传递?

scala> def foo(a:Any) { println("ok") }
scala> foo(1,2,3) // 给出警告,但运行OK
scala> foo() // 给出警告,但运行OK

其实这个问题跟之前的这篇 scala雾中风景(16): println(1,2,3)为什么work? 属于同一个问题,scala编译器发觉对只有一个参数的方法在调用时参数不一致的情况下,会在最后阶段尝试一次“适配”,简单的说就是用”()”进行tuple化,如果参数多于一个,将整个TupleN当作参数传入,如果参数为0,则tupling得到一个Unit类型实例传入。

对于scala编译器在方法参数上自作聪明的“适配”,应该严格禁止它发生的可能,建议所有的项目编译时,都开启 -Yno-adapted-args 让编译器给出错误,避免混乱。

scala类型系统:柯里-霍华德同构

通过柯里-霍华德同构实现联合类型

在这个系列之前的这篇blog scala类型系统:10) 交集类型与联合类型 里曾提到过实现union类型的另一种方式是柯里-霍华德同构

这段代码是从这里看到的:

type ¬[A] = A => Nothing
type ∨[T, U] = ¬[¬[T] with ¬[U]]
type ¬¬[A] = ¬[¬[A]]
type |∨|[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }

def size[T : (Int |∨| String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}

scala> size(3)
res0: Int = 3

scala> size("three")
res1: Int = 5

要理解这段类型证明,先要了解复合类型,结构类型,函数类型的逆变,类型投影,以及type-lambda等,这些点可以从之前的blog里参考。

对于普通的class来说,A with B复合类型是类型AB的子类型:

scala> import scala.reflect.runtime.universe._

scala> class A; class B;

scala> typeOf[A with B] <:< typeOf[A]
res0: Boolean = true

但对于函数类型,因为其参数类型是逆变的,所以情况正好反过来:

scala> typeOf[ A=>Nothing ] <:< typeOf[ (A with B)=>Nothing ]
res1: Boolean = true

用图来看:

 __________               _________________________ 
|          |             |                         |
|     A    |             |  (A with B) => Nothing  |
|__________|             |_________________________|
      ^                               ^ 
      |                               | 
 __________               _________________________ 
|          |             |                         |
| A with B |             |        A => Nothing     |
|__________|             |_________________________|  

所以,我们把上文中定义的:¬[¬[A]]¬[¬[T] with ¬[U]] 两个表达式展开的话,会发现这两个函数类型是有继承关系的(如果参数类型A与T或U相同的话):

//¬[¬[A]]
scala> type T1 = (A=>Nothing) => Nothing  

//¬[¬[T] with ¬[U]]
scala> type T2 = ((A=>Nothing) with (B=>Nothing)) => Nothing  

scala> typeOf[T1] <:< typeOf[T2]
res3: Boolean = true

然后定义了一个结构类型:

type |∨|[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }

这个结构类型里的λ是一个类型工厂,当传入给它某个类型参数时,它会构造出一个新的类型 ¬¬[X] <:< (T ∨ U) ,这个是中缀写法,还原成普通类型写法是: <:<[¬¬[X], (T ∨ U)] 关于 <:< 这个类型是用于约束两个类型关系的,它是在Predef里定义的:

sealed abstract class <:<[-From, +To] extends (From => To) with Serializable

它要求 ¬¬[X] 是类型 T v U (即v[T,U])的子类型。

而这个要求,我们前边证明 ¬[¬[X]]¬[¬[T] with ¬[U]] 的关系时已经论证过了,当类型参数X与T或U一致时即:

// 只有当
X =:= T //或 
X =:= U 

//下面的约束才成立
¬¬[X] <:< (T ∨ U) 

那么现在来单独用λ这个类型构造器,它的表达方式为 (T |v| U)#λ,这是一段type-lambda,它描述的是一个构造器类型,它可以由类型参数X,构造出一个复杂的约束类型:

<:<[(X=>Nothing)=>Nothing, ((T=>Nothing) with (U=>Nothing))=>Nothing]

这个约束类型能够编译通过的前提如前边所说,需要X等于TU才行。

现在我们把用一个具体的,由IntString类型参数构造的 |∨|[Int, String] 类型中的λ类型构造器用Factory来表示:

type Factory[T] = (Int |∨| String)#λ[T]

这个Factory类型构造器,对于任何类型参数 T,构造出的类型展开后为:

<:<[(T=>Nothing)=>Nothing, ((Int=>Nothing) with (String=>Nothing))=>Nothing]

也就是说T必须是IntString才能满足。最后用隐式参数的方式(context bound)在方法定时时描述这种约束

def size[T: Factory](p: T) = ...

相当于:

def size[T](p: T)(implicit ev: Factory[T]) ...

如果你纳闷这个ev隐式参数是哪里来的,参考一下以前的这篇: scala类型系统:22) 类型约束与特定方法

整个过程只涉及到类型证明,与以前的实现方式对比不需要显式提供任何隐式转换或隐式参数(只用到了语言默认提供的一个隐式参数),也没有创建任何“单例”或“实例对象”,柯里-霍华德同构可以简单理解为类型证明(type-lambda)即程序。

scala类型系统:类型推导

类型推导是一门博大的学问,背后有繁冗的理论,好在非编译器开发者不用太多了解,我看到这方面的文章时立刻就知难而退了。这里蜻蜓点水的带过。

Scala采用的局部的(local)、基于流的(flow-based)类型推断;而非像ML,Haskell等语言采用的更加全局化的Hindley-Milner类型推断方式。

在《Programming in Scala》一书中提到基于流的类型推断有它的局限性,但是对于面向对象的分支类型处理比Hindley-Mlner更加优雅。

基于流的类型推导在偏应用函数场景下,不能对参数类型省略,正体现了它的局限性。下面的偏应用函数声明,在Hindley-Milner类型推导(基于全局的)可以正常的推导的,会在scala里报错:

scala> def foo(a:Int, b:String) = a+b

scala> val f = foo(200, _)
<console>:8: error: missing parameter type for expanded function ((x$1) => foo(200, x$1))
   val f = foo(200, _)
                    ^

上面第二个参数占位符缺乏类型。需要显式的声明变量f的类型

scala> val f: String=>String = foo(200, _)
f: String => String = <function1>

或者显式的声明占位符参数类型

scala> val f = foo(200, _:String)
f: String => String = <function1>

不过这里有一个细节是eta-expansion的情况下可以省略参数类型,比如:

scala> def foo(a:Int, b:String) = a+b

scala> val f = foo _
f: (Int, String) => String = <function2>

scala> val f = foo(_,_)
f: (Int, String) => String = <function2>

上面两种写法占位符都是对全部参数,foo _foo(_,_) 在编译过程会编译器会进行eta-expansioneta扩展会把这个表达式转换为与foo方法类型一致的函数对象。这是在规范里定义的。

而最开始的写法 foo(200, _)并不是eta-expansion(原因见之前的这篇)。这种情况下类型推导并不会从方法体的上下文去推断参数的类型。所以需要显式的声明参数类型。

另外,对于泛型方法的情况,类型推导也需要注意泛型参数,参考这篇

scala类型系统:case class与代数数据类型

ADT(algebraic data type) 是很多函数式编程语言中支持的一种类型。不清楚最初是否来自haskell。

《Programming in Scala》中文版,在术语表中有提到ADT:

通过提供若干个含有独立构造器的备选项(alternative)来定义的类型。通常可以辅助于通过模式匹配解构类型的方式。这个概念可以在规约语言和函数式语言中发现。代数数据类型在Scala中可以用样本类(case class)模拟。

参考wiki:http://en.wikipedia.org/wiki/Algebraic_data_type

在计算机编程、特别是函数式编程与类型理论中,代数数据类型(ADT)是一种组合类型(composite type).例如,一个类型由其它类型组合而成。两个常见的代数类型是product(乘积)类型(比如tuplesrecords)和sum类型,它也被称为”tagged unions”或”variant type”

ADT最大的价值是用于“模式匹配”,即解构一个对象。

我们看看Scala怎么用case class模拟ADT,比如一副扑克按花色来分类:

sealed abstract class Suit
case class Heart(value: Int) extends Suit
case class Diamond(value: Int) extends Suit
case class Spade(value: Int) extends Suit
case class Club(value: Int) extends Suit

当然case class的构造参数并没有限制,比如下面这个描述一棵树的节点和叶子:

sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree

关于case class更多细节请参考模式匹配系列blog。

scala类型系统:28) 依赖类型

依赖类型是用在type class模式中的一种场景,当type class中存在多个类型参数时,某个类型参数可以由其他类型参数来决定。

这个概念是从shapeless作者的blog里看到的,依赖类型也是源自haskell的一种扩展

Functional dependencies are a near-standard extension to Haskell (present in GHC and elsewhere) which allow constraints on the type parameters of type classes to be expressed and then enforced by the compiler.They allow the programmer to state that some of the type parameters of a multi-parameter type class are completely determined by the others.

先看一下他举的例子,假设矩阵类型与矢量类型以及Int类型在做笛卡尔乘积运算时,结果类型取决于这两者的组合:

Matrix  *   Matrix  -> Matrix  //矩阵 * 矩阵 得到 矩阵
Matrix  *   Vector  -> Vector  //矩阵 * 矢量 得到 矢量
Matrix  *   Int     -> Matrix  //矩阵 * Int 得到 矩阵 
Int     *   Matrix  -> Matrix  //Int * 矩阵 得到 矩阵

因为结果类型受入参类型的约束,我们在设计函数时,可以把结果类型约束为满足这种组合的情况,用scala来实现

scala> trait Matrix 

scala> trait Vector

scala> trait MultDep[A, B, C] //定义一个笛卡尔运算时需要依赖类型,用于约束结果类型

我们通过隐式对象提供各种类型的组合时的依赖类型:

scala> implicit object mmm extends MultDep[Matrix, Matrix, Matrix]
scala> implicit object mvv extends MultDep[Matrix, Vector, Vector]
scala> implicit object mim extends MultDep[Matrix, Int, Matrix]
scala> implicit object imm extends MultDep[Int, Matrix, Matrix]

检验一下隐式对象:

scala> implicitly[MultDep[Matrix, Matrix, Matrix]] // OK: Matrix * Matrix -> Matrix
scala> implicitly[MultDep[Matrix, Vector, Vector]] // OK: Matrix * Vector -> Vector
scala> implicitly[MultDep[Matrix, Vector, Matrix]] // Error 没有定义过这种组合

现在在笛卡尔积的方法上增加这个隐式参数,来限制参数类型和结果类型必须符合我们的组合要求:

def mult[A, B, C](a : A, b : B)(implicit instance : MultDep[A, B, C]) : C =
{ 
    null.asInstanceOf[ C ]
}

这样我们在运行mult方法时,当传入的参数类型不符合我们用隐式对象提供的组合时,编译器直接会帮我们检查出来:

val r1 : Matrix = mult(new Matrix {}, new Matrix{}) // Compiles
val r2 : Vector = mult(new Matrix {}, new Vector{}) // Compiles
val r3 : Matrix = mult(new Matrix {}, 2)            // Compiles
val r4 : Matrix = mult(2, new Matrix {})            // Compiles
val r5 : Matrix = mult(new Matrix {}, new Vector{}) // 错误,结果类型应该是Vector

在scala的集合库中,当从一种类型的集合转换到另一种类型的集合时,比如执行mapto方法时会调用CanBuildFrom这个隐式参数来判断当前类型是否可转换为目标类型:

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
    val b = bf(repr)
    b.sizeHint(this)
    for (x <- this) b += f(x)
    b.result
}

这个CanBuildFrom type class 里面有三个类型参数:Repr, B, That

而这三个类型其实是有依赖的关系的,即 That 类型,是由ReprB类型决定的:

// 由 List[_], String 构造 List[String]
scala> implicitly[CanBuildFrom[List[Int], String, List[String]]]

// 由 Set[_], Double 构造 Set[Double]
scala> implicitly[CanBuildFrom[Set[String], Double, Set[Double]]]

// 由 List[_], Int 构造的是 List[Int] 与 Set[String] 目标类型不一致
scala> implicitly[CanBuildFrom[List[String], Int, Set[Int]]]

// 由 List[_], Int 构造的是 List[Int] 与 List[Double] 目标类型不一致
scala> implicitly[CanBuildFrom[List[String], Int, List[Double]]]

scala类型系统:27) 回顾常见的type classes

在scala世界,type classes是一种非常常见的模式,我们回顾一下scala库的实现中有哪些采用这种模式的

1) Ordering

scala> def foo[T:Ordering] (l:List[T]) { 
            l.sorted.foreach(print) 
        }

scala> foo(List(2,3,1))
123

Ordering[_]抽象了大于、等于、小于等用于比较的方法。它是一个典型的type class

2) Numeric

scala> def foo[T:Numeric] (l:List[T]) { 
            print(l.sum) // List只对数字类型才能使用sum方法
        }

scala> foo(List(1,2,3))
6

scala> def bar[T:Numeric](a:T, b:T) {
            implicitly[Numeric[T]].plus(a,b) 
        }

scala> bar(2,3)

Numeric[_]对抽象了所有数字类型中的加、减、乘、除等操作。

3) Manifest/TypeTag/ClassTag 等

scala> def biuldArray[T:ClassTag](len:Int) = new Array[T](len)

scala> buildArray[Int](3)
res8: Array[Int] = Array(0, 0, 0)

4) <:<[-From, +To], =:=[From,To]

scala> def test[T](i:T) (implicit ev: T <:< java.io.Serializable) { 
            print("OK") 
        }

不要被它的两个类型参数和中缀方式给迷惑,这两个用于类型证明的泛型类,使用上也属于type class模式

在《scala in depth》中总结了type classes模式的几个优点:

1) Separation of abstractions (抽象分离)
2) Composability (可组合)
3) Overridable (可覆盖)
4) Type safety (类型安全)

scala类型系统:26) type classes模式

在进入主题之前,先了解一下通过泛型实现参数化多态的形式:

trait Comparable[T] { 
    def comp(o:T) = this.hashCode - o.hashCode 
}

class A extends Comparable[A] { 
    override def comp(o:A) = super.comp(o) 
}

class B(val i:Int) extends Comparable[B] { 
    override def comp(o:B) = this.i - o.i 
}

上面定义了一个Comparable的泛型特质,对于A,B等实现者可以设定不同的参数类型。

在scala里因为有隐式转换的功能,也可以在定义时不显式的继承Comparable特质,而在伴生对象里定义一个隐式转换,在需要用comp方法的时候,编译器会自动转换为目标类型:

scala> class A; 

scala> object A { 
            implicit def convert(a:A) : Comparable[A] = {
                new a.type with Comparable[A] { 
                    override def comp(o:A) = a.hashCode - o.hashCode 
                } 
            }
        }   

scala> a.comp(new A)
res2: Int = -1633174887

不过scala已经不鼓励使用隐式转换,必要的话,可以使用隐式参数:

// 需要定义一个新的comparable2特质,comp方法接受2个参数
scala> trait Comparable2[T] { def comp(x:A, y:A) = x.hashCode - y.hashCode } 

scala> class A; 

scala> object A { 
            implicit val compA = 
                new Comparable2[A] { override def comp(o:A) = super.comp(o) }
        }

上面定义了一个隐式参数compA它是Comparable[A]类型,不过这样如果要用到A的comp行为,需要在调用的方法中声明隐式参数

// 柯里化,最后一个参数是隐式的,编译器会自动在上下文找

scala> def call[T](x:T, y:T) (implicit c : Comparable2[T]) { 
            c.comp(x,y) 
        }

scala> val x = new A; val y = new A;

scala> call(x,y)

这样做相当于把A的结构与行为(方法)给分离开了(解耦);或者说对某个类型,在不改动其本身的情况下,具备了扩展其行为的能力。

这个模式被称为type classes,源于haskell,我对haskell不熟悉,为什么用这么奇怪的一个名字?从这儿看到:在haskell里没有类似于java/scala里的class的概念,所以class这个术语可以随意使用,它的意思相当于”class of types”,可以理解为对某一系列类型的抽象(即高阶类型)。

scala里的type classes模式主要通过隐式参数来实现,但需要注意的是,并不是所有的隐式参数都可以理解为type classes模式,隐式参数的类型必须是泛型的(高阶),它表示某种抽象行为,这种行为的具体实现要由它的具体类型参数决定。

// 隐式参数c在这里是多态的
scala> def call[T](x:T, y:T) (implicit c : Comparable2[T])

// 针对A实现了一个具体的comp行为,它的参数类型是A 
scala> implicit object CompA extends Comparable2[A] { 
            override def comp(x:A, y:A) = super.comp(x, y) 
        }

简单总结,type classes模式通过泛型来描述某种通用行为,对每个想要用到这种行为的具体类型,在现实时行为部分并不放在类型自身中,而是通过实现一个type class实例(对泛型具化),最后在调用时(通过隐式参数)让编译器自动寻找行为的实现。

scala类型系统:23) 用类型证明实现联合类型

我们用类型证明来实现一个联合类型(union type):

// 让参数类型满足Int或String

scala> def f[A](a: A)(implicit ev: (Int with String) <:< A) = println("OK")
f: [A](a: A)(implicit ev: <:<[Int with String,A])Unit

scala> f(2) 
OK

scala> f("hi")
OK

跟我们之前通过context bound来是实现联合类型,对比一下更简洁。

scala类型系统:22) 类型约束与特定方法

对于类型限制 =:=<:<

A =:= B  //表示A类型等同于B类型
A <:< B  //表示A类型是B类型的子类型

这个看上去很像操作符的=:=<:<,实际是一个类,它在Predef里定义:

sealed abstract class =:=[From, To] extends (From => To) with Serializable

sealed abstract class <:<[-From, +To] extends (From => To) with Serializable

它定义了两个类型参数,所以可以使用中缀写法:From <:< To

2.10之前还有一个 <%< 类似于view bound,表示 A可以当作B,即A隐式转换成B也满足。但在2.10里已经废弃这种写法。

类型限制用在特定方法(specialized methods)的场景,所谓特定,是指方法只针对特定的类型参数才可以运行:

scala> def test[T](i:T)(implicit ev: T <:< java.io.Serializable) { print("OK") }
test: [T](i: T)(implicit ev: <:<[T,java.io.Serializable])Unit

scala> test("hi")
OK

scala> test(2)
<console>:9: error: Cannot prove that Int <:< java.io.Serializable.

上面定义的test方法,在方法的第二个参数使用了一个隐式参数ev,它的类型是:T <:< java.io.Serializable,表示只有参数类型Tjava.io.Serializable的子类型,才符合类型要求。

或许你会奇怪上面test方法调用”hi”时,隐式参数ev是从哪儿传入的?当前并没有定义这个隐式参数。这个隐式参数也是由Predef里的隐式方法产生的

private[this] final val singleton_<:< = new <:<[Any,Any] { def apply(x: Any): Any = x }

implicit def conforms[A]: A <:< A = singleton_<:<.asInstanceOf[A <:< A]

当调用test("hi"),编译器推断出T是String,在寻找 String <:< java.io.Serializable类型的隐式参数时,上下文中找不到,于是通过conforms隐式方法来产生一个,conforms方法只有一个类型参数,它产生的结果是<:<[String,String]类型的对象,但因为<:<[-From,+To]第一个类型参数是逆变的,第二个类型参数是协变的,所以<:<[String,String]符合<:<[String,java.io.Serializable]的子类,满足要求。

而调用test(2)时,因为隐式方法产生的<:<[Int,Int]不符合<:<[Int,java.io.Serializable]子类型,抛出了异常。可见这块编译器是利用函数类型的多态机制来实现类型检测的。

另外,对于Type类型,在判断之间的关系时也有类似的写法,不过这里是Type类型的方法:

scala> typeOf[List[_]] =:= typeOf[List[AnyRef]]
res4: Boolean = false

scala> typeOf[List[Int]] <:< typeOf[Iterable[Int]]
res1: Boolean = true

上面的是方法调用:typ1.=:=(typ2) ,虽然效果都是证明类型关系,但不要混淆。

//2013.11.8 补充
昨天泽彬在csug的旺旺群(94329267/csugcsug)里问<:<:<的差异:

object A{
    def test[T <: java.io.Serializable](i:T) {}
    test(1) // 编译时报错

    def test2[T](i:T)(implicit ev: T <:< java.io.Serializable)  {}
    test2(1) // 同样编译时报错
}

两者的效果似乎一样,应该怎么选择?他在stackoverflow上也问了一下:http://stackoverflow.com/questions/19829770/whats-different-between-and-in-scala

有人给出这样的解释:

def foo[A, B <: A](a: A, b: B) = (a,b)

scala> foo(1, List(1,2,3))
res1: (Any, List[Int]) = (1,List(1, 2, 3))

传入第一个参数是Int类型,第二个参数是List[Int],显然这不符合 B <: A 的约束,编译器在做类型推导的时候,为了满足这个约束,会继续向上寻找父类型来匹配是否满足,于是在第一个参数被推导为Any类型的情况下,List[Int] 符合Any的子类型。

def bar[A,B](a: A, b: B)(implicit ev: B <:< A) = (a,b)

scala> bar(1,List(1,2,3))
<console>:9: error: Cannot prove that List[Int] <:< Int.

通过隐式参数ev来证明类型时,类型推断过程不会像上面那样再向上寻找可能满足的情况,而直接报错。

确实,在用 <: 声明类型约束的时候,不如用<:<更严格,除了上面的类型推导,存在隐式转换的情况下:

scala> def foo[B, A<:B] (a:A,b:B) = print("OK")

scala> class A; class B;

scala> implicit def a2b(a:A) = new B

scala> foo(new A, new B)  //存在A到B的隐式转换,也可满足
OK

scala> def bar[A,B](a:A,b:B)(implicit ev: A<:<B) = print("OK")

scala> bar(new A, new B)  //隐式转换并不管用
<console>:17: error: Cannot prove that A <:< B.

scala类型系统:21) type specialization与类爆炸

Type Specialization的paper,这里也已经有翻译了。简单的说,就是出于性能考虑,避免基础类型的装箱拆箱,为基础类型保留了特定的实现。

class V[@specialized T]{
    var i:T = _
}

上面用了specialized注释,除了定义一个泛型的V[A],之外,编译器还会额外生成9个特定的类型:

% scalac V.scala && ll
-rw-r--r--  1 hongjiang  wheel  1060 11  5 16:07 V$mcB$sp.class
-rw-r--r--  1 hongjiang  wheel  1070 11  5 16:07 V$mcC$sp.class
-rw-r--r--  1 hongjiang  wheel  1066 11  5 16:07 V$mcD$sp.class
-rw-r--r--  1 hongjiang  wheel  1063 11  5 16:07 V$mcF$sp.class
-rw-r--r--  1 hongjiang  wheel  1065 11  5 16:07 V$mcI$sp.class
-rw-r--r--  1 hongjiang  wheel  1060 11  5 16:07 V$mcJ$sp.class
-rw-r--r--  1 hongjiang  wheel  1063 11  5 16:07 V$mcS$sp.class
-rw-r--r--  1 hongjiang  wheel  1032 11  5 16:07 V$mcV$sp.class
-rw-r--r--  1 hongjiang  wheel  1063 11  5 16:07 V$mcZ$sp.class
-rw-r--r--  1 hongjiang  wheel  3549 11  5 16:07 V.class
-rw-r--r--  1 hongjiang  wheel    41 11  5 16:07 V.scala

hongjiang@whj-mbp /tmp/dd % javap V\$mcD\$sp     
Compiled from "V.scala"
public class V$mcD$sp extends V{
    public double i$mcD$sp;
    public double i$mcD$sp();
    public double i();              // 这个是针对double类型的
    public void i$mcD$sp_$eq(double);
    public void i_$eq(double);
    public boolean specInstance$();
    public void i_$eq(java.lang.Object);
    public java.lang.Object i();
    public V$mcD$sp();
}

从上面看到,哪些匿名类的类名中的:B,C,D,F,I,J,S,V,Z,分别对应 byte,char,double,float,int,long,short,void(scala里的Unit), boolean

在类型参数比较多的情况下,specialization会产生类爆炸的情况,参考stackoverflow上的一个例子

class Huge[@specialized A, @specialized B, @specialized C](
  val a: A, val b: B, val c: C
) {} // 730 files, 2.9 MB

上面的类产生了730个文件:9x9x9+1。之前在模拟perm gen的oom时,用到过这个特性。

//2012.5.30
在REPL下,模拟perm gen的oom案例:

虽然perm gen确实在不断增加,并且可能会OOM(如果perm gen设置比较小的话),但通过jconsole的类加载来看并非把这些定义的class都load了;对比了前后的class load

之前:3797 个class被load

运行一次:

class Test[@specialized A, @specialized B, @specialized C, @specialized D]

之后:4180 ,增加了383个而不是 9x9x9x9=6561个 why?

通过jvisualvm工具dump了前后的内存对比,scala.tools.nsc.io.VirtualFile 实例数从2增加到了 6573,增长了 6571 个,并且每运行一次都会增加 6571个,比6561多出10个

继续测试

class Test[@specialized A] 

也是会使 VirtualFile 新增 19个而非9个,另外多出来的10个实例还没有清楚是什么。