史上最简单的Scala for推导教程

Scala 的 for  语法,本质上就是语法糖。并且十分简单,简单到简短的一篇文章,就可以说清楚。

Scala 中的 for 语句,都是帮你转换成类似 foreachmapflatMapwithFilterfilter 之类的语法。并且整个转化过程是递归的,容易理解的。

看完下面是几个例子,你就能立马掌握。这几个例子,是结合了 Scala 官方的yield文档 改进过来的。

第一个例子,转换成 foreach

会被scalac翻译成

注意,没有出现 yield 关键字。

再来一个:

会展开成:

然后 scalac会再针对里面的 for 再进行转换,直到所有的 for 被转换完。

第二个例子,先 flatMapmap

会被 scalac翻译成

注意,这里出现了 yield关键字。

再来一个:

会展开成:

然后再展开成

注意这里多了一个 yield 关键字,所以会用 flatMapmap 而不用 foreach

并且,前面的都使用 forMap(比如 c1),最后面使用 map(比如 c2)。

好了,有无 yield的区别讲完了,这也是最重要的区别了,剩下的都很简单,而且不管有无 yield,概念都一样。

接着往下。

第三个例子,尝试 withFilter ,不行再 filter

会翻译成

如果 c 没有 withFilter 方法,就使用 filter 方法:

第四个例子,加入其它值

会变成

结束

好了,scala 的 for 规则就结束了,就是这么简单。你肯定能记住。

Scala trait 的线性化规则详解 -2

接上文,Scala 为了解决多重继承的问题,使用了线性化。那么线性化的规则是什么?我们什么时候需要了解 Scala 的线性化规则?

我们先来看看以下问题(来自 Programming in Scala 3rd,并进行加强):

在上面的代码中,打印出来的结果是什么?想1分钟,然后看答案。

答案是:

如果你能够正确推导出答案,那就不必往下读了。如果你推导不正确,或者无从下手,那么请接着往下读,我会尽我可能讲明白的。

线性化的规则是什么?

Scala 线性化的规则在 Scala 的语言规范 中有讲(56页, 5.1.2 Class Linearization)。但篇幅很少,较难理解,我将用自己的理解来进行接下来的说明。

线性化解决的是将 A extends B with C with D... 展开成单条继承链的问题。

线性化的规则理解有以下几点:

  1. extendswith 的区别: extends 是继承, with 是继续。
  2. with 的优先级比 extends
  3. 单条继承链中,类不能有重复

以上三条就是线性化的规则。读者可能一下子不太明白。接下来,我将使用这三条规则,结合上面提到的问题进行展开。

我们重新看下 Cat 的定义: class Cat extends Animal with Furry with FourLegged, 开始推导其线性化。为了能够讲明白,我只做了几张图,结合图片来说明可能会更有效。

首先,我们看 Cat 的定义:

Cat定义

根据规则2, extendswith 的优先级低,因此先展开上图中虚线框住的部分。即展开 Animal with Furry with FourLegged。此时,Cat 的线性化结果还是空的,我们用 result = List() 表示。

接着开始展开 Animal。此时我们进入 Animal,将 Animal 进行线性展开(对,这个线性展开的过程是递归的)。如图:

因为在 Scala 中,AnyRef 和 Any 都是基本的类,所以就先忽略掉它们。因此, Animal 的线性展开就是 Animal。此时,我们的结果就是 result = List("Animal")。同时, Animal 也已经展开完成。

接下来开始线性化 FurryFurry 的定义如下图:

很容易看出, Furry 的线性展开结果是 Furry, Animal

于是我们将 Furry, Animal 放到 result = List("Animal") 中,结果是 result = List("Furry", "Animal", "Animal")。为什么放在 result的前面?因为根据规则1, with 不是继承,而是继续。

但根据规则3,类不能有重复,于是 Animal必须去掉一个。去掉哪一个呢?因为之前的 result中已经有一个 Animal 占坑了,所以后加入的 Animal只能被去掉,因此结果现在为 result = List("Furry", "Animal")

到目前为止,我们已经进展到下图了:

很好,已经完成了 AnimalFurry 了,而目前的线性化阶段性结果是 result = List("Furry", "Animal")。接下来该进行 FourLegged的线性展开了。

这里可以看到, FourLegged 是继承了 HasLegs的,而 HasLegs 又是继承了 Animal。因此, FourLegged 的线性化展开结果是 FourLegged, HasLegs, Animal。再和总体的 result = List("Furry", "Animal") 进行结合,结果是 result = ("FourLegged", "HasLegs", "Furry", "Animal")。 是的, Animal 又重复了,又被去掉了。 所以,我们现在的情况是:

已经把相关 with的进行了线性展开,结果是 result = ("FourLegged", "HasLegs", "Furry", "Animal"),这时候再把 Cat 加上,结果便是 result = ("Cat","FourLegged", "HasLegs", "Furry", "Animal")。线性化展开到此结束。

因此,上面程序的打印结果 Cat FourLegged HasLegs Furry Animal 也就很自然了。

我们什么时候需要了解 Scala 的线性化规则

trait 的地方,就需要了解线性化的规则。这使你明白每个 super 所代表的含义。

同时,我们也需明白, trait 的线性化可以玩得很复杂,即使我们能明白其中的秘密,用户却不一定能够明白。如果我们玩得太复杂,用户就不会买账。

因此,这也要求我们在设计的时候,一定要避免这样混乱的继承关系,避免让使用者混乱。能够一目了然的线性关系,对使用者是最好的体验。

最后,请尝试使用程序描述 Scala 的线性化算法。下一篇我也会提供相应的代码。

Scala trait 的线性化规则详解 -1

多重继承的问题

如你所知,Scala 的 traitmixin 的功能,非常有用、灵活,但会带来一个多重继承的问题(菱形问题)。如下简单代码:

多重继承的问题并不是 Scala 独有,其他语言也都有这样的问题。 Java 8 之前只允许单继承,没有这个问题,但 Java 8 在接口上引入 default方法 之后,也有了同样的问题。对此, Java 8 的解决方案很简单,就是由用户代码指定,如下代码所示:

其中 A.super.f() 就是用户手工指定该使用哪个方法。

Scala 的解决方案

对于多重继承,Scala 给出的解决方案是线性化,术语是 linearization。

什么是 linearization?即是将多重继承转换成单继承,从而解决多重继承带来的问题。

那 Scala 线性化的规则是什么?请看下文Scala trait 的线性化规则详解 -2