标签归档:pitfalls

scala雾中风景(11): isInstanceOf与类型擦拭

scala中用isInstanceOf判断一个对象实例是否是某种类型时,如果类型包含参数类型,会被擦拭掉(jvm的做法)。所以会导致例如下面的问题:

scala> (1,2).isInstanceOf[Tuple2[String,String]]

<console>:11: warning: fruitless type test: a value of type (Int, Int) cannot also be a     (String, String) (but still might match its erasure)
          (1,2).isInstanceOf[Tuple2[String,String]]
                            ^
res33: Boolean = true

在给出了一段警告之后,结果返回true,如果数据对象支持模式匹配且元素较少,可以用模式匹配来精确判断:

scala> (1,2) match{ case(_:Int, _:Int) => println("ok"); case _ => "no"}
ok

但元素比较多的话,或数据不支持模式匹配,要判断参数类型,就需要让编译器在运行时保持参数类型的信息了,在scala里是通过TypeTag来实现的,参考scala类型系统:19) Manifest与TypeTag

scala> def checkType[A: TypeTag](a: A, t: Type) =  typeOf[A] <:< t

scala> checkType((1,2), typeOf[Tuple2[String,String]])
res37: Boolean = false

scala> checkType((1,2), typeOf[Tuple2[Int,Int]])
res38: Boolean = true

scala雾中风景(10): 逆变点与协变点

这个问题来自之前这篇scala类型系统:15) 协变与逆变的评论里的问题

遇见一个这样的问题

class In[+A]{ def fun(x:A){} }

会提示

error: covariant type A occurs in contravariant position in type A of value x
class In[+A]{def fun(x:A){}}
                     ^

而这样不会出现问题

class In[-A]{ def fun(x:A){} }

要解释清楚这个问题,需要理解协变点(covariant position) 和 逆变点(contravariant position)

首先,我们假设 class In[+A]{ def fun(x:A){} } 可以通过编译,那么对于 In[AnyRef]In[String] 这两个父子类型来说,fun方法分别对应:

父类型 In[AnyRef] 中的方法 fun(x: AnyRef){}

子类型 In[String] 中的方法 fun(x: String){}

根据里氏替换原则,所有使用父类型对象的地方都可以换成子类型对象。现在问题就来了,假设这样使用了父类:

father.fun(notString)

现在替换为子类:

child.fun(notString) // 失败,非String类型,不接受

之前父类型的 fun 可以接收 AnyRef类型的参数,是一个更广的范围。而子类型的 fun 却只能接收String这样更窄的范围。显然这不符合里氏替换原则了,因为父类做的事情,子类不能完全胜任,只能部分满足,是无法代替父类型的。

所以要想符合里氏替换,子类型中的fun函数参数类型必须是父类型中函数参数的超类(至少跟父类型中的参数类型一致),这样才能满足父类中fun方法可以做的事情,子类中fun方法也都可以做。

正是因为需要符合里氏替换法则,方法中的参数类型声明时必须符合逆变(或不变),以让子类方法可以接收更大的范围的参数(处理能力增强);而不能声明为协变,子类方法可接收的范围是父类中参数类型的子集(处理能力减弱)。

方法参数的位置称为做逆变点(contravariant position)。所以上面声明类型参数A是协变的,用在方法参数中时会编译报错,声明A是逆变(或不变)时则符合。

现在看看什么是协变点(covariant position),还用上面的例子,稍微修改一下:

// 方法返回值类型可以是协变的
scala> class In[+A]{ def fun(): A = null.asInstanceOf[A] }
defined class In

// 方法返回值类型不能是逆变的
scala> class In[-A]{ def fun(): A = null.asInstanceOf[A] }
<console>:8: error: contravariant type A occurs in covariant position in type ()A of method fun

同样用里氏替换法则来套:

父类型 In[AnyRef] 中的方法 fun() 得到结果 AnyRef

子类型 In[String] 中的方法 fun() 得到结果 String

这是很容易理解的,子类方法得到的结果比父类更“具象”一些,也就是说子类方法的处理能力更强一些。如果结果类型是逆变的,那子类方法的处理能力是减弱的,不符合里氏替换。

方法返回值的位置称为协变点(covariant position)。同理,A类型声明协变(或不变),编译时符合要求;声明逆变则报错。

现在我们再回顾:

class In[-A] { def fun(x: A) {} } 

我们完全可以把它看做一个函数类型,即 A => UnitFunction1[-A, Unit]等价,而

class In[+A]{ def fun(): A = null.asInstanceOf[A] }

则与 Function0[+A] 等价。

另外参考:
1) scala中函数类型的多态
2) scala类型系统:16) 函数类型

scala雾中风景(9): List(1,2,3) == Seq(1,2,3) ?

惜朝在来往的扎堆里问:

scala> List(1, 1, 2) == Seq(1, 1, 2)
res219: Boolean = true

scala里Seq和List是一会儿事?

这个问题归根到底在于 == 在集合里是怎么实现的?在scala里==的语义等同于java里的equals,我们跟踪一下

val a = List(1,2,3)
val b = Seq(1,2,3)
a.equals(b)     // 设置断点

注意,在a.equals(b)出设置断点,scala-ide不一定能进入内部逻辑,你还是需要在它父类equals方法内设置断点才行。

上面的equals实际会到 GenSeqLike.equals,见下图:

它的逻辑是判断两个集合是否“可比较”(canEqual),如果可比较,则判断内部的元素是否相同:

override def equals(that: Any): Boolean = that match {
    case that: GenSeq[_] => (that canEqual this) && (this sameElements that)
    case _               => false
}   

于是我们推测 List(1,2,3)Seq(1,2,3)的容器类型应该是相同的类型或有继承关系,但是进入到canEqual逻辑内部,无法验证这个判断,它直接返回true,按理说应该对两个容器的类型进行比较一下才合适(看来只是给用户实现的集合类型实现equals时留了一个扩展点,scala自己的集合类型并不做类型判断)。

接下来进入 sameElements 逻辑,因为List混入了LinearSeqOptimized特质,这块的逻辑是在LinearSeqOptimized中的,见下图:

我们看到它通过模式匹配,要求目标集合也必须是LinearSeq类型。然后迭代并比较了两个容器内的各个元素是否相同,都相同的话就认为两个容器也相同。不过从这里的逻辑我们也可以判断出来,两个容器equals为true的话,并不一定需要是完全同样的类型或者有父子关系。我们验证一下:

1)两个集合分别是Seq特质与Set特质下的子类,是两种不一样的集合

scala> List(1,2,3) == Set(1,2,3)
res28: Boolean = false

2) 两个集合都是Set特质下的子类

scala> HashSet(1,2,3) == TreeSet(1,2,3)
res29: Boolean = true

3) 两个集合都是Seq特质下的子类

scala> ListBuffer(1,2) == LinkedList(1,2)
res20: Boolean = true

4) 两个集合都是Seq特质下的子类,不过QueueLinearSeq下的,而RangeIndexedSeq下的

scala> Queue(1,2) == Range(1,2)
res18: Boolean = false

5) 两个集合都是Seq特质下的子类,Seq(1,2,3)的实现是?

scala> Range(1,2,3) == Seq(1,2,3)
res12: Boolean = false

第1种情况好理解,ListSet 毕竟是另种含义不同集合,Set的实现也不会是LinearSeq特质的,所以返回false.第2种也容易理解,两个集合都是Set特质下的。

问题是3,4,5,为何Seq下会有多种情况,这还要我们再全面的看一下scala的集合框架,借用
这里的图片:

正是因为 Seq 特质下,又分为了IndexedSeqLinearSeq 两个分支,并且这两个特质中各自对 sameElements的逻辑有不同的实现,使得IndexedSeq的集合与LinearSeq下的集合比较时不可能相等。

另,对于 List(1,2,3)Seq(1,2,3)在构造集合的背后逻辑,可以参考这篇:通过List.apply方法构造List的背后逻辑

scala雾中风景(8): 高阶函数与Unit的谜题

这个问题是在spray-routing的使用过程中发现的,又是一个“障眼法”问题。

简化一下,假定有下面的类型:

scala> class Request

scala> type Route = Request => Unit

Route是一个函数类型,然后我们定义一个接受Route类型的方法

scala> def hf(r:Route) { println("ok") }
hf: (r: Request => Unit)Unit

现在传递一个Route类型的实例,调用hf方法:

scala>  hf( req => {} )
ok

上面传递了一个 req => {} 的函数对象,运行没有问题。

我们再定义一个Route的生成器:

scala> def routeGenerator: Route = req => println("do nothing")
routeGenerator: Request => Unit

把这个生成器作为参数传递给 hf 方法:

scala> hf(routeGenerator)
ok

跟刚才没什么区别。

现在我们如果传入一个看上去像是高阶函数的函数: req => routeGenerator,hf方法还会接受吗?

scala> hf ( req => routeGenerator )
ok

是不是有点奇怪?我传入的不是 Request => Route 类型吗?展开的话应该是 Request => (Request => Unit) 为什么hf方法也能接受呢?

这里核心的问题就在于 req => routeGenerator 这个函数实例,究竟是什么类型?这与编译器的类型推导逻辑有关。

当我们定义一个变量,不声明类型,把上面的函数对象赋给这个变量:

scala> val param =  (req:Request) => routeGenerator
param: Request => (Request => Unit) = <function1>

变量param的类型是 Request => (Request => Unit) 类型,与我们预测的一致。

如果定义param时,指定它的类型是RouteRequest => Unit,上面的函数对象还可以赋值吗?

scala> val param: Route  =  (req:Request) => routeGenerator
param: Request => Unit = <function1>

这里困惑我们的是,为什么函数对象右边的routeGenerator的类型在这次的上下文中变成了Unit,而不是我们定义的Route类型了呢?

如果你对Unit类型真的了解(参考之前的这篇:scala雾中风景(4): Unit类型),这个时候就不会被迷惑了。

因为Unit类型自身的特点,在赋值时,可以把任意类型的表达式赋值给它:

scala> val tmp:Unit = "hello"
tmp: Unit = ()

因为表达式背后会被翻译为: { "hello", () },同理,在之前的上下文里,定义了 param 是一个 Reuqest => Unit
类型的变量,在赋值时,编译器就会把 req => routeGenerator 翻译成:

req => { routeGenerator; () } 

这个问题看上去像是个高阶函数的问题,实际与高阶函数没关系。至于Unit类型为何在给变量赋值时设计成这样,可能与函数式语言的历史上已经是这样设计了,scala很可能是从ML那块继承的这个设计。

或许我们可以把Unit类型在赋值时,理解成一个带有副作用的”过程”,这个过程接受无论什么类型的表达式,执行这些表达式,但最终返回的是()这个值。