前言 最近要改行做大数据相关的东西了,经调研大数据开发的语言还是用 Scala 好,当然 Java 也可以,毕竟都运行在 JVM 上,不过 Java 也有很长时间没用过了,所以对于 Shaun 来说用 Scala 和 Java 的代价是一样的,都需要学习一下,所以决定用对大数据更友好的 Scala。
以 Martin Odersky 14 年写的「Scala By Example」为参考,虽然是 14 年的,但 Scala 的基本语法还是没变的,就学习本身而言没问题,毕竟不兼容的只是更上层的 API,Shaun 学习用的 Scala 版本为 2.12.12。Alvin Alexander 的「Scala Cookbook, 2nd Edition」预计今年 8 月会出版,到时可能这本书用来入门更好,但 Shaun 不需要系统的学,就简单的能上手写出比较理想的 Scala 代码就行了。
学习篇 第一章:入门基础 HelloWorld 由于「Scala By Example」第一章没啥内容,也为了在正式写 Scala 之前简单熟悉一下,这里先用「A Scala Tutorial for Java Programmers」简单上手一下,首先写个 HelloWorld,具体代码如下:
1 2 3 4 5 object HelloWorld { def main (args: Array [String ]) { println("Hello, world!" ) } }
和 C 语言类似,程序唯一入口函数都是 main 函数,但 Scala 的变量在前,声明的类型在后,相比常规的语言是有点奇怪了,但这种语法规则和 Typescript 一样,所以很容易接受,但其模板的表示就有点奇怪了,Array[String] 表示一个 String 类型的数组,即表示方法为 Array[T],常规的模板方式为 Array<T>
或 T[]
,def 关键字用来定义一个函数,object 用来表示一个单例类,即在定义类的同时,又创建了一个类的实例。Scala 中没有 static 关键字,需要用 static 修饰的都放在 object 中即可。
调用 Java Scala 中默认已导入 java.lang 中的全部类,但其它类需要显式导入,以格式化输出本地日期为例:
1 2 3 4 5 6 7 8 9 10 import java.util.{Date , Locale }import java.text.DateFormat ._object LocalDate { def main (args: Array [String ]) { val now = new Date val df = getDateInstance(LONG , Locale .CHINA ) println(df format now) } }
Scala 中的导入和 java 中 import 基本一样,但功能更强大,可以使用 {}
导入部分,也使用 _
导入全部(java 导入全部为 *
,这不一样),当一个函数只有一个参数,可以通过 空格+参数 的形式调用,而不需要使用 括号包裹 的形式。这里采用 val
关键字声明的是常量,而要声明变量需要用 var
。
对象 Scala 中万物皆对象,一个数字也是一个对象,一个函数也是一个对象,具体如下图:
enter image description here 以简单计时器函数为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 object Timer { def oncePerSecond (callback: () => Unit ) { while (true ) { callback(); Thread sleep 1000 ; } } def timeFiles () { println("time files like an arrow..." ); } def main (args: Array [String ]) { oncePerSecond(() => { println("time files like an arrow..." ); }); } }
这个和 Typescript 函数式编程的用法基本差不多,唯一不同这里声明的函数返回的是 Unit
,这个 Unit 可认为是无返回的函数,大部分情况等同于 void,在 Scala 中真正的没有值指的是 Nothing。
类 Scala 中同样有类,具体代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Complex (real: Double , imaginary: Double ) { def re = real; def im = imaginary; override def toString (): String = "" + re + (if (im < 0 ) "" else "+" ) + im + "i" ; } object ComplexNumbers { def main (args: Array [String ]) { val c = new Complex (1.2 , -3.4 ); println(c.toString()); } }
在 Scala 中所有类都会继承某个父类,若没有显式声明父类,则默认继承 scala.AnyRef 类,如上面的 Complex 类,若需要覆盖父类的函数,则需要在函数声明前加上 override 关键字。当函数没有参数时,可以不用加括号,在调用时也不用加括号,如上面示例的注释和非注释的代码。
模式匹配与条件类 接下来用 Scala 来写一个树结构表示表达式的示例代码,树的非叶节点表示操作符,叶子节点表示数值(这里为常量或变量),具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 abstract class Tree case class Sum (l: Tree , r: Tree ) extends Tree case class Var (n: String ) extends Tree case class Const (v: Int ) extends Tree object Expression { type Environment = String => Int def eval (t: Tree , env: Environment ): Int = t match { case Sum (l, r) => eval(l, env) + eval(r, env) case Var (n) => env(n) case Const (v) => v } def derive (t: Tree , v: String ): Tree = t match { case Sum (l, r) => Sum (derive(l, v), derive(r, v)) case Var (n) if (v == n) => Const (1 ) case _ => Const (0 ) } def main (args: Array [String ]) { val exp: Tree = Sum (Sum (Var ("x" ), Var ("x" )), Sum (Const (7 ), Var ("y" ))) val env: Environment = {case "x" => 5 case "y" => 7 } println("Expression: " + exp) println("Evalution with x=5, y=7: " + eval(exp, env)) println("Derivative relative to x:\n" + derive(exp, "x" )) println("Derivative relative to y:\n" + derive(exp, "y" )) } }
该示例主要用来说明两种 case 关键字,分别为:case class 和 ... match case ...,前者可认为是一个结构体,实例化时可以省略 new 关键字,参数有默认的 getter 函数,整个 case class 有默认的 equals 和 hashCode 方法实现,通过这两个方式可实现根据值判断类的两个实例是否相等,而不是通过引用,条件类同样有默认的 toString 方法实现;后者可认为是一种特殊的 switch case ,只不过 case 的判定和执行是函数式的,case class 可直接参与 match case 的判定(判定是不是属于该类)。第 7 行中有个 type 关键字,可认为是定义了一种新的类型(不是数据类型),示例中是函数类型,通过这个 type ,可直接将字符串映射为整型,23 行中将这个 type 与 case 结合使用,定义多个字符串映射多个整型的变量。第 18 行中有个 _
,这是 scala 中的通配符,不同的语义下表示的含义不同,这里的含义是指,当上面的模式都不匹配时,将执行这个,相当于 switch case 中的 default。
Scala 中的 trait 简单理解就是 Java 中的 Interface(接口),Scala 中没有 interface 关键字,但是 trait 比 Interface 的功能更多,其中可直接定义属性和方法的实现,Scala 中可通过 trait 来实现多重继承。下面的示例用 trait 简单实现了一个比较接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 trait Ord { def < (that: Any ): Boolean def <= (that: Any ): Boolean = (this < that) || (this == that) def > (that: Any ): Boolean = !(this <= that) def >= (that: Any ): Boolean = !(this < that) } class Date (y: Int , m: Int , d: Int ) extends Ord { def year = y def month = m def day = d override def toString (): String = year + "-" + month + "-" + day override def equals (that: Any ): Boolean = { that.isInstanceOf[Date ] && { val o = that.asInstanceOf[Date ] o.day == day && o.month == month && o.year == year } } def < (that: Any ): Boolean = { if (!that.isInstanceOf[Date ]) { sys.error("cannot compare " + that + " and a Date" ) } val o = that.asInstanceOf[Date ] (year < o.year) || (year == o.year && (month < o.month || (month == o.month && day < o.day))) } } object Comparable { def main (args: Array [String ]) { val d1 = new Date (2021 , 1 , 3 ); val d2 = new Date (2021 , 1 , 3 ); println(d1 < d2) println(d1 <= d2) } }
比较关系一般只需要确定 小于 和 等于 关系即可,其它关系都可由这两关系推出来,由于等于方法默认存在于所有对象中,所以只需要重写小于即可, 其它的比较方法都可以在 trait 中定义好。在上面的示例中有两个函数 isInstanceOf 和 asInstanceOf,前者用来判断对象是否是指定类型,后者用来将对象转换为指定类型,一般用在将父类转为子类时,在使用 asInstanceOf 之前一般需要先使用 isInstanceOf。
泛型 这东西没啥好说的,基本有编程经验的或见过或用过,只是 Scala 的泛型语法确实有点奇怪就是了,可能也是为了函数式那些乱七八糟的操作符,具体示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Reference [T ] { private var contents: T = _ def set (value: T ) { contents = value } def get : T = contents } object IntegerReference { def main (args: Array [String ]) { val cell = new Reference [Int ] cell.set(13 ) println("Reference contains the half of " + (cell.get * 2 )) } }
这里同样有个 _
,这里表示的是默认值,对于数字类型来说是 0,对于 boolean 来说是 false,对于 Unit(函数签名)来说是()(无参数无返回),对于其他来说是 null。
简单的了解 Scala 就到这里了。
第二章:快排 开场就是一个快排,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 object QuickSort { def qSort (xs: Array [Int ]) { def swap (i: Int , j: Int ) { val t = xs(i); xs(i) = xs(j); xs(j) = t; } def sort (l: Int , r: Int ) { val pivot = xs(l); var i = l+1 ; var j = r; while (i < j) { while (i <= r && xs(i) < pivot) i += 1 ; while (j > l && xs(j) > pivot) j -= 1 ; if (i < j) { swap(i, j); i += 1 ; j -= 1 ; } if (i > j) { i = j; } } while (i > l && xs(i) > pivot) { i -= 1 ; j -= 1 ; } swap(i, l); if (l < j-1 ) sort(l, j-1 ); if (j+1 < r) sort(j+1 , r); } sort(0 , xs.length-1 ); } def main (args: Array [String ]) { val xs = Array (4 , 1 , 5 , 7 ,7 ,7 ,7 , 2 , 6 ); qSort(xs); println(xs.mkString(" " )) } }
从这段快排代码可看出,Scala 支持函数嵌套和闭包,即在函数内部定义子函数,子函数可直接使用父函数的变量,同时,这里也简单说明一下 Scala 中数组的一些使用方法,用下标取数组元素时使用的是小括号 ()
,而不是其它语言常见的中括号 []
。当然 Scala 作为一种函数式语言,提供了非常多的函数式操作符,这篇也只会简单介绍。
第三章:Actor Actor,Scala 中的多线程编程模型,下方的示例代码在 Scala 2.11 及之后的版本无法运行,因为 Actor 已从 Scala 库独立出来,见 object-actors-is-not-a-member-of-package-scala 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import scala.actors.Actor abstract class AuctionMessage case class Offer (bin: Int , client: Actor ) extends AuctionMessage case class Inquire (client: Actor ) extends AuctionMessage abstract class AuctionReply case class Status (asked: Int , expire: Date ) extends AuctionReply case object BestOffer extends AuctionReply case class BeatenOffer (maxBid: Int ) extends AuctionReply case class AuctionConCluded (seller: Actor , client: Actor ) extends AuctionReply case object AuctionFailed extends AuctionReply case object AuctionOver extends AuctionReply class Auction (seller: Actor , minBid: Int , closing: Date ) extends Actor { val timeToShutdown = 36000000 val bidIncrement = 10 def act () { var maxBid = minBid - bidIncrement var maxBidder: Actor = null var running = true while (running) { receiveWithin ((closing.getTime() - new Date ().getTime())) { case Offer (bid, client) => { if (bid >= maxBid + bidIncrement) { if (maxBid >= minBid) maxBidder ! BeatenOffer (bid) maxBid = bid; maxBidder = client; client ! BestOffer } else { client ! BeatenOffer (maxBid) } } case Inquire (client) => { client ! BeatenOffer (maxBid) } case TIMEOUT => { if (maxBid >= minBid) { val reply = AuctionConCluded (seller, maxBidder) maxBidder ! reply; seller ! reply } else { seller ! AuctionFailed } receiveWithin(timeToShutdown) { case Offer (_, client) => client ! AuctionOver case TIMEOUT => running = false } } } } } } class HelloActor extends Actor { def act () { while (true ) { receive { case name: String => println("Hello, " + name) } } } } object AuctionService { def main (args: Array [String ]) { val seller: Actor = new HelloActor val client: Actor = new HelloActor val minBid = 10 val closing = new Date () val helloActor = new HelloActor helloActor.start() helloActor ! "leo" } }
通过重写 Actor 中的 act
方法即可简单的实现多线程编程,Actor 中有个特殊的标识符 !
,该符号其实是是一种缩写,即可将 helloActor.!("leo")
缩写为 helloActor ! "leo"
,代表将数据传递给 Actor,由 Actor 内部的 receive case
接受数据并处理,当然也可通过 receiveWithin
控制数据传递时间,若超时,则默认触发 TIMEOUT
处理模式。
第四章:表达式与简单函数 该章主要有两个例子:1、牛顿法求平方根;2、尾递归,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 object Sqrt { def sqrt (x: Double ): Double = { def sqrtIter (guess: Double , x: Double ): Double = { if (isGoodEnough(guess, x)) guess else sqrtIter(improve(guess, x), x) } def improve (guess: Double , x: Double ) = { (guess + x / guess) / 2 } def isGoodEnough (guess: Double , x: Double ) = (guess * guess - x).abs < 0.001 sqrtIter(1.0 , x) } } object TailRecursion { def gcd (a: Int , b: Int ): Int = if (b == 0 ) a else gcd(b, a % b) def facorial (n: Int ): Int = if (n == 0 ) 1 else n * facorial(n-1 ) def facorialTail (n: Int ): Int = { def facorialIter (n: Int , res: Int ): Int = { if (n == 0 ) res else facorialIter(n-1 , res * n) } facorialIter(n, 1 ) } } object SimpleFunc { def main (args: Array [String ]) { val sqrtValue = Sqrt .sqrt(0.01 ) println(sqrtValue) val gcdValue = TailRecursion .gcd(14 ,21 ) println(gcdValue) val facorialValue = TailRecursion .facorial(5 ) println(facorialValue) val facorialTailValue = TailRecursion .facorialTail(5 ) println(facorialTailValue) } }
由于并没有引入新的语法,就简单聊聊这两个例子吧。牛顿法求平方根主要在于构造一个特殊的二分函数 \(y_{i+1} = (y_i + x / y_i)/2, i=0,1,2,3,..., y_0=1\) ,如此迭代,直到 \(|y_i^2-x| < \epsilon\) ,得到 \(y_i\) 即为 x 的平方根,更朴素一点的求多次方根就是利用二分法,分 [0, 1] 和 [1, +∞] 两个区间即可,对应从 [x, 1] 和 [1, x] 开始二分取值。至于尾递归,以前简单的写过一点,即最后递归调用原函数时,原函数不会再参与任何计算表达式。尾递归的好处在于当编译器或解释器支持尾递归时,将不会产生普通递归时的压栈操作,即不用担心递归层次太深,尾递归将类似循环迭代处理。
第五章:高阶函数 高阶函数(First-Class Functions),支持以函数作为参数或返回值,也可将函数赋值给其它变量,由此也可引出闭包和柯里化,闭包是指将内嵌函数作为返回值,而柯里化是指将多个参数分解为独立参数传递给函数,如:\(f(args_1,args_2,...,args_n)=f(args_1)(args_2)(...)(args_n)\) 。下面以求函数的不动点为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 object FirstClassFunctions { val tolerance = 0.0001 def isCloseEnough (x: Double , y: Double ) = ((x-y) / x).abs < tolerance def fixedPoint (f: Double => Double )(firstGuess: Double ) = { def iterate (guess: Double ): Double = { val next = f(guess) if (isCloseEnough(guess, next)) next else iterate(next) } iterate(firstGuess) } def averageDamp (f: Double => Double )(x: Double ) = (x + f(x)) / 2 def sqrt (x: Double ) = fixedPoint(averageDamp(y => x/y))(1.0 ) def main (args: Array [String ]) { println(sqrt(0.01 )); } }
该示例简单明了的展示了 Scala 中匿名函数,函数柯里化以及闭包。
第六章:类和对象 直接看下面的有理数示例吧,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Rational (n: Int , d: Int ) extends AnyRef { private def gcd (x: Int , y: Int ): Int = { if (x == 0 ) y else if (x < 0 ) gcd(-x, y) else if (y < 0 ) -gcd(x, -y) else gcd(y % x, x) } private val g = gcd(n, d) def this () { this (0 , 0 ) } val number: Int = if (g != 0 ) n / g else 0 val denom: Int = if (g != 0 ) d / g else 0 def + (that: Rational ) = new Rational (number * that.denom + that.number * denom, denom * that.denom) def - (that: Rational ) = new Rational (number * that.denom - that.number * denom, denom * that.denom) def * (that: Rational ) = new Rational (number * that.number, denom * that.denom) def / (that: Rational ) = new Rational (number * that.denom, denom * that.number) def toNumber : Double = if (denom != 0 ) number.toDouble / denom else 0.0 override def toString = "" + number + "/" + denom } object Rational { def main (args: Array [String ]) { val rational = new Rational (2 ,1 ) / new Rational () println(rational.toNumber); println(rational.toString); } }
从有理数这个示例可以看出,Scala 的类支持操作符重载,也支持构造函数重载,同样支持继承,多继承也是支持的,每个父类用 with
关键字分隔就行。
第七章:条件类和模式匹配 大致和第一章内容差不多,就不重复写了。
第八章:泛型 大致也和第一章内容差不多,值得一提的书中实现的泛型栈本质是一个链表,实现方法挺有意思的 。通过 <:
标识符可约束泛型的类型,如 [T <: P[T]]
表明泛型 T 必须类型 P 的子类型。而标识符 <%
比 <:
约束性弱一点,只要 T 能够通过隐式类型变换为 P 即可。若想约束为父类型,则需使用 >:
标识符。
Scala 中有一种特殊的泛型,就是变化型注解,trait List[+T]
代表协变,表示当 B 类型是 A 类型子类时,List[B]
也可认为是 List[A]
的子类;trait List[-T]
代表逆变,当 B 类型是 A 类型子类时,List[B]
可认为是 List[A]
的父类。
Scala 中同样有元组,使用时也很方便,简单使用直接用括号声明即可,如 def divmod(x: Int, y: Int): (Int, Int) = (x / y, x % y)
,该函数即返回一个元组,也可声明一个元组 case class Tuple2[A, B](_1: A, _2: B)
,若需要取元组的元素可通过 _i
的方式,如 val xy = divmod(3, 4); xy._1; xy._2;
,也可通过 match-case 语句取,如 xy match { case (n, d) => println("quotient: " + n + ", rest: " + d) }
。
第九章:List Scala 中的 List 其实是数组结构,并且是不可变的,可认为是 C++ 里的静态数组,不能往其中添加或删除元素,下面用数组排序示例下 List 的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 object Sort { def insertSort (xsl: List [Int ]): List [Int ] = { def insert (x: Int , xs: List [Int ]): List [Int ] = { xs match { case List () => List (x) case y :: ys => if (x <= y) x :: xs else y :: insert(x, ys) } } if (xsl.isEmpty) Nil else insert(xsl.head, insertSort(xsl.tail)) } def mergeSort [A ](less: (A , A ) => Boolean )(xs: List [A ]): List [A ] = { def merge (xs1: List [A ], xs2: List [A ]): List [A ] = { if (xs1.isEmpty) xs2 else if (xs2.isEmpty) xs1 else if (less(xs1.head, xs2.head)) xs1.head :: merge(xs1.tail, xs2) else xs2.head :: merge(xs1, xs2.tail) } val n = xs.length / 2 if (n == 0 ) xs else merge(mergeSort(less)(xs take n), mergeSort(less)(xs drop n)) } def main (args: Array [String ]) { val xs = List (4 , 1 , 5 , 7 ,7 ,7 ,7 , 2 , 6 ); println(xs(0 ), xs(1 ), xs(xs.length-1 )) val ys = mergeSort((x: Int , y: Int ) => x > y)(xs); println(ys.mkString(" " )) } }
List 中有两个操作符非常类似,即 ::
和 :::
, 前者用于 List 中的元素和 List 连接,即创建一个新 List,新 List 为原 List 头插入元素后的 List,后者用于连接两个 List,即创建一个新 List ,新 List 为将第二个 List 的元素全部放入第一个 List 尾部的 List。字符 Nil
代表空 List 和 List()
等效,head
方法返回 List 的第一个元素,tail
方法返回除第一个元素之外的其它所有元素,还是一个 List,isEmpty
方法当 List 为空时返回 true
。List 的 case-match 方法中,case y :: ys
其中 y 代表 xs.head,ys 代表 xs.tail。(xs take n)
表示取 List 前 n 个元素,(xs drop n)
表示取 List 前 n 个元素之外的元素,即与 (xs take n) 取得元素正好互补,而 (xs split n)
返回一个元组,元组中第一个元素为 (xs take n),第二个元素为 (xs drop n)。关于 List 还有些更高阶得方法:filter,map, flatMap, reduceRight, foldRight 等方法就不继续写了。至于动态 List 可用 ListBuffer
结构,当然 Scala 中直接用 Seq
作为返回值和参数一般会更好些。
第十章:序列理解 Scala 中用来做序列理解的表达式是 For-Comprehensions
,具体示例如下:for (p <persons if p.age > 20) yield p.name
相当于 persons filter (p => p.age > 20) map (p => p.name)
,可以简单认为 for-yield 方法是 filter 和 map 的集合体。下面具体用个 N-皇后(特例是 8 皇后)的示例来具体说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 object NQueen { def queens (n: Int ): List [List [Int ]] = { def isSafe (col: Int , queenList: List [Int ], delta: Int ): Boolean = { val curRow = queenList.length-1 + delta for (row <- List .range(0 , queenList.length)) { val queenCol = queenList(row) val queenRow = queenList.length-1 - row if (queenCol == col) return false if (queenRow == curRow) return false if ((queenCol - col).abs == (queenRow - curRow).abs) return false } true } def placeQueens (k: Int ): List [List [Int ]] = { if (k == 0 ) List (List ()) else for { queens <- placeQueens(k-1 ); column <- List .range(0 , n); if isSafe(column, queens, 1 ) } yield column :: queens } placeQueens(n) } def main (args: Array [String ]) { val queenList = queens(8 ); println("queenCount: " + queenList.length) } }
for-yield 表达式中 for 中可以写多条语句,代表多重循环,第 5 行的 for 代表 for 循环,<-
表示取 List 中的元素。
剩下的几章就没啥特别要写的,重点就两个特性,一个是 Stream ,一个 Lazy,Stream 和 List 有点类似,主要区别在于 Stream 是即时返回的,算一个返回一个,而 List 一般是全部计算完再返回一个 List;Lazy 一般用作常量的修饰符,主要作用是只用该常量被用到时才赋值,否则一直为空,有点类似常见的先判空再取值的封装。
后记 曾看到过通过刷题去学习新语言的方式,一直都以为很粗暴,但这次照着「Scala By Example」敲下来,感觉还挺有效的,同时也巩固了一下基本的算法知识,后续再把 twitter 的 「Effective Scala」再看一下应该就差不多了。