使用Scala语言进行编程

Scala和Kotlin、Clojure等一样是一种jvm语言,传说其复杂度可与C++一较高下。用下来感觉并不舒服,例如其中的implicit特性,能够减少很多代码的冗余,但另一方面,又会导致代码对新手而言的可读性变差。
这篇文章拆分自我从前的文章《使用Scala进行Spark-GraphX编程》。

括号

通常,小括号()表示表达式和函数调用,大括号{}表示代码块。例如在.map({})中的大括号即表示一个代码块。特别地,代码块也是一个表达式,所以下面的代码也是成立的

1
( { var x = 0; while (x < 10) { x += 1}; x } % 5) + 1

同时,括号也是可以省略的。根据Scala Style Guide,在Scala中,一个无参方法在调用时可以省略小括号。这里注意,如果函数带一个是隐式参数或者默认参数,那么就不能带空括号。
那么如何区分obj.attribute是字段还是方法呢?对此,Scala有统一访问原则(Uniform Access Principle, UAP),也就是指代码不因为属性是通过字段实现还是方法实现而受影响。因此实际上Scala只有两个命名空间,类型和值。

Implicit

在Scala中,可以通过implicit关键字修饰方法/变量、参数、类,对应实现隐式视图和隐式参数。

隐式视图

隐式视图可以实现隐式Casting。如下面的代码所示,错误的原因是没有办法将Double转为Complex,所以和其他例如C++等语言类似,这里需要一个隐式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case class Complex(r: Double, i: Int) {
def +(c: Complex) = Complex(r+c.r, i+c.i)
def +(d: Double) = Complex(r+d, i)
override def toString() = r + "_" + i
}

val c = Complex(1, 2)
println((c + 1).toString()) // 2.0_2
println((1 + c).toString()) // Error
println(Complex(1, 0) + c) // OK

// 视图绑定要求t能够隐式转换为Complex
def printComplex[T <% Complex](t: T) = {println(t.toString)}
printComplex(1.0)

如下所示,implicitConvert负责Double到Complex的隐式转换

1
implicit def implicitConvert(x: Double) = Complex(1.0, 0)

此外,隐式视图还可以使用目标类的方法来扩展原类的方法。

隐式参数

首先,Scala提供默认函数值,如

1
2
3
def addInt(a: Int, b: Int = 1) : Int = {
return a + b
}

但另一种机制implicit parameter会更为灵活。implicit parameter的用法如下面所示,我们可以为类型People提供一个默认值,这样当我们在调用getName时,就可以给出参数p

1
2
3
4
5
6
7
8
9
case class People(name: String){

}
implicit val ip = People("Calvin")
def getName(implicit p: People) = p.name
def getNameExplicit(p: People) = p.name

getName
getNameExplicit(People("Calvin"))

可以看到,在一定程度上,默认函数可以起到和隐式参数一样的效果,那么为什么还会存在这个特性呢,在爆栈网上有人给出了下面的解释。

函数与方法

柯里化

Scala函数都是柯里函数,因此支持链式地调用,也支持偏/部分应用(注意偏/部分应用和部分函数是两个概念)

高阶函数

使用compose可以实现复合函数

scala> (((x: Int) => x + 1) compose ((y: Int) => y * 2)).apply( 11 )
res1: Int = 23

模式匹配

Scala使用case来实现类似guard的机制。

解构绑定

Scala可以利用样本类case class来实现对象的解构绑定。
case class实际上可以看做对class的语法糖,根据Scala的说明,case class的使用场景就是用来做Structured binding的。

泛型

逆变与协变

逆变(contravariant)和协变(covariant)是在泛型类语境下的。假设B extends A,也就是BA的子类。根据里氏替代原则,在不声明逆变协变的情况下,默认是不变的,也就是C[A]C[B]是雷锋和雷峰塔的关系。
那么协变C[+T]场景下C[B]C[A]的子类。一个常见的例子是CatAnimal的子类,那么我们也自然希望List[Cat]List[Animal]的子类,这样我们的List[Animal]可以接受诸如List[Dog]List[Cat]之类的参数。
然而在逆变C[-T]场景下,C[A]C[B]的子类了。看起来反直觉,但实际上是有作用的。例如我们定义了函数Action[Animal]Action[Cat],顾名思义,我们认为Action[Animal]能够正确处理Animal[Cat],因此我们的Action[Cat]能够接受Action[Animal]作为参数是合理的。

下界与上界

类型下界形如U >: T,表示UT的父类,反之,类型上界S <: T,表示ST的子类。这个符号的箭头方向永远指向孩子。
通常来说,协变常常被用在容器类、返回值上。逆变通常被用在函数和参数上。根据Luca Cardelli规则,就是对输入类型是逆变的,对输出类型是协变的。直观地说,也就是我们可以返回一个更精确的类型(例如返回Object的子类String),接受一个更宽泛的类型。那么这里那里有“泛型”呢?其实我们可以假定一个父类P中有个返回Object的方法,而子类C有个返回String的方法,可以看到P :> CObject :> String,于是协变的关系从这里得到了。
虽然参数设为逆变导致我们可以接受更为宽泛的泛型类。所以我们通过类型下界来限定我们接受的参数U必须是T的父类。

1
2
3
4
5
6
7
class Consumer[+T](t: T) {
// covariant type T occurs in contravariant position in type T of value t
def use(t: T) = {}
}
class Consumer[+T](t: T) {
def use[U >: T](u : U) = {println(u)}
}

Reference

  1. https://docs.scala-lang.org/zh-cn/tour/tour-of-scala.html
  2. https://www.zhihu.com/question/35339328
  3. https://scastie.scala-lang.org/
  4. https://stackoverflow.com/questions/27414991/contravariance-vs-covariance-in-scala
  5. https://twitter.github.io/scala_school/zh_cn/advanced-types.html
  6. https://colobu.com/2015/05/19/Variance-lower-bounds-upper-bounds-in-Scala/
  7. https://www.zybuluo.com/zhanjindong/note/34147