2

Scala 学习小结

 2 years ago
source link: https://cniter.github.io/posts/8d3d87a2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

  最近要改行做大数据相关的东西了,经调研大数据开发的语言还是用 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,具体代码如下:

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 中的全部类,但其它类需要显式导入,以格式化输出本地日期为例:

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) // df format(now)
}
}

  Scala 中的导入和 java 中 import 基本一样,但功能更强大,可以使用 {} 导入部分,也使用 _ 导入全部(java 导入全部为 *,这不一样),当一个函数只有一个参数,可以通过 空格+参数 的形式调用,而不需要使用 括号包裹 的形式。这里采用 val 关键字声明的是常量,而要声明变量需要用 var

Scala 中万物皆对象,一个数字也是一个对象,一个函数也是一个对象,具体如下图:

enter image description hereenter image description here

以简单计时器函数为例:

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(timeFiles);
oncePerSecond(() => {
println("time files like an arrow...");
});
}
}

  这个和 Typescript 函数式编程的用法基本差不多,唯一不同这里声明的函数返回的是 Unit ,这个 Unit 可认为是无返回的函数,大部分情况等同于 void,在 Scala 中真正的没有值指的是 Nothing。

Scala 中同样有类,具体代码示例如下:

class Complex(real: Double, imaginary: Double) {
// def re() = real;
// def im() = imaginary;
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("real part: " + c.re() + " imaginary part: " + c.im());
println(c.toString());
}
}

  在 Scala 中所有类都会继承某个父类,若没有显式声明父类,则默认继承 scala.AnyRef 类,如上面的 Complex 类,若需要覆盖父类的函数,则需要在函数声明前加上 override 关键字。当函数没有参数时,可以不用加括号,在调用时也不用加括号,如上面示例的注释和非注释的代码。

模式匹配与条件类

  接下来用 Scala 来写一个树结构表示表达式的示例代码,树的非叶节点表示操作符,叶子节点表示数值(这里为常量或变量),具体代码如下:

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 简单实现了一个比较接口:

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 的泛型语法确实有点奇怪就是了,可能也是为了函数式那些乱七八糟的操作符,具体示例代码如下:

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 就到这里了。


第二章:快排

开场就是一个快排,示例代码如下:

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, 2, 5, 6);
// val xs = Array(1, 2, 4, 4, 55, 5, 6);
// val xs = Array(55, 6, 6);
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

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 // msec
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、尾递归,具体如下:

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 // guess * guess == x

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)
}
}

  由于并没有引入新的语法,就简单聊聊这两个例子吧。牛顿法求平方根主要在于构造一个特殊的二分函数 yi+1=(yi+x/yi)/2,i=0,1,2,3,...,y0=1yi+1=(yi+x/yi)/2,i=0,1,2,3,...,y0=1 ,如此迭代,直到 |y2i−x|<ϵ|yi2−x|<ϵ ,得到 yiyi 即为 x 的平方根,更朴素一点的求多次方根就是利用二分法,分 [0, 1] 和 [1, +∞] 两个区间即可,对应从 [x, 1] 和 [1, x] 开始二分取值。至于尾递归,以前简单的写过一点,即最后递归调用原函数时,原函数不会再参与任何计算表达式。尾递归的好处在于当编译器或解释器支持尾递归时,将不会产生普通递归时的压栈操作,即不用担心递归层次太深,尾递归将类似循环迭代处理。

第五章:高阶函数

  高阶函数(First-Class Functions),支持以函数作为参数或返回值,也可将函数赋值给其它变量,由此也可引出闭包和柯里化,闭包是指将内嵌函数作为返回值,而柯里化是指将多个参数分解为独立参数传递给函数,如:f(args1,args2,...,argsn)=f(args1)(args2)(...)(argsn)f(args1,args2,...,argsn)=f(args1)(args2)(...)(argsn)。下面以求函数的不动点为例:

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 中匿名函数,函数柯里化以及闭包。

第六章:类和对象

直接看下面的有理数示例吧,

// 主构造函数
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 的用法:

object Sort {
def insertSort(xsl: List[Int]): List[Int] = {
def insert(x: Int, xs: List[Int]): List[Int] = {
xs match {
// case Nil => List(x)
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);
// val xs = 3::2::1::1::Nil;
println(xs(0), xs(1), xs(xs.length-1)) // (4,1,6)
// val ys = insertSort(xs);
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 皇后)的示例来具体说明:

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) // 92
}
}

for-yield 表达式中 for 中可以写多条语句,代表多重循环,第 5 行的 for 代表 for 循环,<- 表示取 List 中的元素。


  剩下的几章就没啥特别要写的,重点就两个特性,一个是 Stream ,一个 Lazy,Stream 和 List 有点类似,主要区别在于 Stream 是即时返回的,算一个返回一个,而 List 一般是全部计算完再返回一个 List;Lazy 一般用作常量的修饰符,主要作用是只用该常量被用到时才赋值,否则一直为空,有点类似常见的先判空再取值的封装。

  曾看到过通过刷题去学习新语言的方式,一直都以为很粗暴,但这次照着「Scala By Example」敲下来,感觉还挺有效的,同时也巩固了一下基本的算法知识,后续再把 twitter 的 「Effective Scala」再看一下应该就差不多了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK