scala
auggie

scala tutorial

scala & spark

简介 & 安装 & helloworld

  • Scala 是一门多范式的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。

  • 静态类型

  • Scala 运行在 Java 虚拟机上,并兼容现有的 Java 程序。

  • Scala 源代码被编译成 Java 字节码,所以它可以运行于 JVM 之上,并可以调用现有的 Java 类库。

1
brew install coursier/formulas/coursier && cs setup
Commands Description
scalac the Scala compiler
scala the Scala REPL and script runner
scala-cli Scala CLI, interactive toolkit for Scala
sbt, sbtn The sbt build tool
amm Ammonite is an enhanced REPL
scalafmt Scalafmt is the Scala code formatter

Hello world

  1. cd to an empty folder.
  2. Run the command sbt new scala/scala3.g8 to create a Scala 3 project, or sbt new scala/hello-world.g8 to create a Scala 2 project.
  3. input project name
  4. Run sbt. This opens up the sbt console.
  5. Type ~run.

scalafmt 配置 一点都不智能还需要配置,不如 gofmt

vscode 配置 code

变量

  • val 可变的
  • var 不可变

在 Scala 中,通常创建变量而不声明它们的类型。Scala 通常可以推断数据类型。

1
val x = 1

也可以显示声明:

1
val x: Int = 1

枚举

sealed trait

  • 在使用模式匹配的时候,使用 sealed 修饰某个 class 的目的是让 Scala 知道所有 case 的情况,否则会编译报错。

控制结构

if/else

if/else 类似 c,但是存在返回值。所以可以直接当成三目运算符。

1
val x = if (a < b) a else b

match

强大的匹配表达式是 Scala 的一大特点,scala 的 match 非常强大。

1
2
3
4
5
val result = i match {
case 1 => "one"
case 2 => "two"
case _ => "not 1 or 2"
}

match表达式不仅限于整数,它可以与任何数据类型一起使用,包括布尔值:

1
2
3
4
val booleanAsString = bool match {
case true => "true"
case false => "false"
}

match这是一个用作方法主体的示例,并与许多不同的类型进行匹配:

1
2
3
4
5
6
7
8
def getClassAsString(x: Any):String = x match {
case s: String => s + " is a String"
case i: Int => "Int"
case f: Float => "Float"
case l: List[_] => "List"
case p: Person => "Person"
case _ => "Unknown"
}

try/catch

1
2
3
4
5
6
try {
writeToFile(text)
} catch {
case fnfe: FileNotFoundException => println(fnfe)
case ioe: IOException => println(ioe)
}

for

1
2
3
4
5
6
7
for (arg <- args) println(arg)

// "x to y" syntax
for (i <- 0 to 5) println(i)

// "x to y by" syntax
for (i <- 0 to 10 by 2) println(i)

您还可以将yield关键字添加到 for 循环以创建产生结果的 for 表达式。这是一个将序列 1 到 5 中的每个值加倍的 for 表达式:

1
val x = for (i <- 1 to 5) yield i * 2

另一种 for

1
2
3
4
5
6
7
8
val fruits = List("apple", "banana", "lime", "orange")

val fruitLengths = for {
f <- fruits
if f.length > 4
} yield f.length

// 使用 {} 可以在里面添加 if 语句

while

Scala 也有whiledo/while循环。这是它们的一般语法:

1
2
3
4
5
6
7
8
9
10
11
12
// while loop
while(condition) {
statement(a)
statement(b)
}

// do-while
do {
statement(a)
statement(b)
}
while(condition)

class

通过 val 和 var 来控制成员变量的可变性。

1
2
3
4
5
6
class Person(val age: Int, var name: String) {
def print(): Unit = {
println(age, name)
}

}

getter 和 setter 方法:

1
2
3
4
5
6
class Counter {
private var privateValue = 0 //变成私有字段,并且修改字段名称
def value = privateValue //定义一个方法,方法的名称就是原来我们想要的字段的名称
def value_=(newValue: Int){
if (newValue > 0) privateValue = newValue //只有提供的新值是正数,才允许修改
}

constructor function

scala 的构造函数非常牛逼,Scala的主构造器是整个类体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person(var firstName: String, var lastName: String) {

println("the constructor begins")

// 'public' access by default
var age = 0

// some class fields
private val HOME = System.getProperty("user.home")

// some methods
override def toString(): String = s"$firstName $lastName is $age years old"

def printHome(): Unit = println(s"HOME = $HOME")
def printFullName(): Unit = println(this)

printHome()
printFullName()
println("you've reached the end of the constructor")

}

auxiliary class constructors 辅助构造函数

  • 使用 this 命名
  • 其它类似函数重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val DefaultCrustSize = 12
val DefaultCrustType = "THIN"

// the primary constructor
class Pizza (var crustSize: Int, var crustType: String) {

// one-arg auxiliary constructor
def this(crustSize: Int) = {
this(crustSize, DefaultCrustType)
}

// one-arg auxiliary constructor
def this(crustType: String) = {
this(DefaultCrustSize, crustType)
}

// zero-arg auxiliary constructor
def this() = {
this(DefaultCrustSize, DefaultCrustType)
}

override def toString = s"A $crustSize inch pizza with a $crustType crust"

}
1
2
3
4
val p1 = new Pizza(DefaultCrustSize, DefaultCrustType)
val p2 = new Pizza(DefaultCrustSize)
val p3 = new Pizza(DefaultCrustType)
val p4 = new Pizza

带默认值的构造函数

1
2
3
class Socket(var timeout: Int = 2000, var linger: Int = 3000) {
override def toString = s"timeout: $timeout, linger: $linger"
}

方法

1
2
3
4
5
6
7
def add(a: Int, b: Int): Int = {
a + b
}
// 不能使用 var 和 val

def sub(a: Int, b: Int): Int = ???
// 使用 ???,类似于 python 的 pass

匿名函数

1
2
scala> val boo = (i: Int) => i * 2
val boo: Int => Int = Lambda$1748/964687142@190a5f74

可以配合 map , filter, reduce使用

1
2
3
4
5
6
7
8
9
10
11
scala> var ints = List.range(0, 10)
var ints: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> ints.filter((i: Int) => i > 5)
val res12: List[Int] = List(6, 7, 8, 9)

scala> ints.map((i: Int) => i * 2)
val res13: List[Int] = List(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)

scala> ints.reduce((a: Int, b: Int) => a + b)
val res14: Int = 45

常用序列函数

1
2
a 方法 b
a.方法(b)

上面二者是等价的。所以:

1
2
books flatMap (s => s.toList)
books.flatMap(s => s.toList)

闭包

闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。

1
2
3
var factor = 3  
val multiplier = (i:Int) => i * factor
// multiplier 是一个闭包

接口

scala trait 相当于 java interface,但是更像是 java 的抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Speaker {
def speak(): String
}

trait TailWagger {
def startTail(): Unit
def stopTail(): Unit
}

trait Runner {
def startRunning(): Unit
def stopRunning(): Unit
}

一旦你有了这些小块,你就可以Dog通过扩展它们来创建一个类,并实现必要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dog extends Speaker with TailWagger with Runner {

// Speaker
def speak(): String = "Woof!"

// TailWagger
def startTail(): Unit = println("tail is wagging")
def stopTail(): Unit = println("tail is stopped")

// Runner
def startRunning(): Unit = println("I'm running")
def stopRunning(): Unit = println("Stopped running")

}
  • 使用 extendswith 实现多扩展

可以在变量初始化的时候动态追加特征☕️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait Stop {
def stop() = println("stop")
}

trait Run {
def run(): Unit = println("running")
}

class Dog(var name: String = "Dog", var age: Int = 10) {
override def toString(): String = s"$name $age"
}

object Main extends App {
val d = new Dog with Run with Stop
d.run
}

抽象类

1
2
3
4
5
6
7
8
9
10
abstract class Animal(name: String = "Animal") {
def speak(): Unit = println(name)
}

class Dog(name: String = "Dog") extends Animal(name)

object Main extends App {
val d = new Dog
d.speak()
}

Object

object的构造器不接受参数传递。

单例对象

使用object中的常量或方法,通过object名直接调用,对象构造器在对象第一次被使用时调用(如果某对象一直未被使用,那么其构造器也不会被调用)。

Scala并没有提供Java那样的静态方法或静态字段,但是,可以采用object关键字实现单例对象,具备和Java静态方法同样的功能。

1
2
3
4
5
6
7
8
object Person {
private var lastId = 0 //一个人的身份编号
def newPersonId() = {
lastId +=1
lastId
}
}
printf("The id is %d.\n",Person.newPersonId())

伴生对象

当单例对象与某个类同名时,它被称为这个类的“伴生对象”。类和它的伴生对象必须存在于同一个文件中,而且可以相互访问私有成员(字段和方法)。

apply 方法和 update 方法

apply 约定如下:用括号传递给变量(对象)一个或多个参数时,Scala 会把它转换成对apply方法的调用;

  • 用括号传递给 变量 调用 class 的 apply
  • 用括号传递给 对象 调用 object 的 apply
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TestApplyClassAndObject {
}
class ApplyTest{
def apply() = println("apply method in class is called!")
def greetingOfClass: Unit ={
println("Greeting method in class is called.")
}
}
object ApplyTest{
def apply() = {
println("apply method in object is called")
new ApplyTest()
}
}
object TestApplyClassAndObject{
def main (args: Array[String]) {
val a = ApplyTest() //这里会调用伴生对象中的apply方法
a.greetingOfClass
a() // 这里会调用伴生类中的apply方法
}
}

在Scala中,伴生对象有一个重要用途,那就是,我们通常将伴生对象作为工厂使用,这样就不需要使用关键字new来创建一个实例化对象了。

1
2
3
4
5
6
class Car(name: String){
def info() {println("Car name is "+ name)}
}
object Car {
def apply(name: String) = new Car(name) //apply方法会调用伴生类Car的构造方法创建一个Car类的实例化对象
}

update 与此相似的,当对带有括号并包括一到若干参数的对象进行赋值时,编译器将调用对象的update方法,在调用时,是把括号里的参数和等号右边的对象一起作为update方法的输入参数来执行调用

1
2
3
4
val myStrArr = new Array[String](3) //声明一个长度为3的字符串数组,每个数组元素初始化为null
myStrArr(0) = "BigData" //实际上,调用了伴生类Array中的update方法,执行myStrArr.update(0,"BigData")
myStrArr(1) = "Hadoop" //实际上,调用了伴生类Array中的update方法,执行myStrArr.update(1,"Hadoop")
myStrArr(2) = "Spark" //实际上,调用了伴生类Array中的update方法,执行myStrArr.update(2,"Spark")

从上面可以看出,在进行元组赋值的时候,之所以没有采用Java中的方括号myStrArr[0],而是采用圆括号的形式,myStrArr(0),是因为存在上述的update方法的机制。

高级数据结构

ArrayBuffer

类似于 c++ 的 vector

要使用ArrayBuffer你必须先导入它:

1
import scala.collection.mutable.ArrayBuffer

C

1
2
3
4
val ints = ArrayBuffer[Int]()
val names = ArrayBuffer[String]()

val nums = ArrayBuffer(1, 2, 3)

R

1
arr(2)

U

推荐使用 ++,而不是 append 耗时比较

1
2
3
4
5
6
7
8
// add one element
nums += 4

// add multiple elements
nums += 5 += 6

// add multiple elements from another collection
nums ++= List(7, 8, 9)

D

1
2
3
4
5
6
7
8
// remove one element
nums -= 9

// remove multiple elements
nums -= 7 -= 8

// remove multiple elements using another collection
nums --= Array(5, 6)

作为简要概述,您可以使用以下几种方法ArrayBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val a = ArrayBuffer(1, 2, 3)         // ArrayBuffer(1, 2, 3)
a.append(4) // ArrayBuffer(1, 2, 3, 4)
a.appendAll(Seq(5, 6)) // ArrayBuffer(1, 2, 3, 4, 5, 6)
a.clear // ArrayBuffer()

val a = ArrayBuffer(9, 10) // ArrayBuffer(9, 10)
a.insert(0, 8) // ArrayBuffer(8, 9, 10)
a.insertAll(0, Vector(4, 5, 6, 7)) // ArrayBuffer(4, 5, 6, 7, 8, 9, 10)
a.prepend(3) // ArrayBuffer(3, 4, 5, 6, 7, 8, 9, 10)
a.prependAll(Array(0, 1, 2)) // ArrayBuffer(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val a = ArrayBuffer.range('a', 'h') // ArrayBuffer(a, b, c, d, e, f, g)
a.remove(0) // ArrayBuffer(b, c, d, e, f, g)
a.remove(2, 3) // ArrayBuffer(b, c, g)

val a = ArrayBuffer.range('a', 'h') // ArrayBuffer(a, b, c, d, e, f, g)
a.dropInPlace(2) // ArrayBuffer(c, d, e, f, g)
a.dropRightInPlace(2) // ArrayBuffer(c, d, e)

scala.util.Sorting.quickSort(fruits) // sort

Array 类似于 python tuple,ArrayBuffer 类似于 python list。可以调用toArray()和toBuffer()转换。

List

The List class is a linear, immutable sequence. All this means is that it’s a linked-list that you can’t modify. Any time you want to add or remove List elements, you create a new List from an existing List.

初始化

1
2
3
val names = List("Joel", "Chris", "Ed")
val names: List[String] = List("Joel", "Chris", "Ed")
val list = 1 :: 2 :: 3 :: Nil

添加元素

由于 List 是单链表结构,所以在首尾添加快,中间慢。

Tip: If you want to prepend and append elements to an immutable sequence, use Vector instead.

1
val a = List(1,2,3)

You prepend elements to a List like this:

1
val b = 0 +: a

and this:

1
val b = List(-1, 0) ++: a

The REPL shows how this works:

1
2
3
4
5
scala> val b = 0 +: a
b: List[Int] = List(0, 1, 2, 3)

scala> val b = List(-1, 0) ++: a
b: List[Int] = List(-1, 0, 1, 2, 3)

Vector

The Vector class is an indexed, immutable sequence.

创建,添加类似 List

Map (mutable)

To use the mutable Map class, first import it:

1
import scala.collection.mutable.Map

初始化

1
2
3
4
5
val states = Map(
"AK" -> "Alaska",
"IL" -> "Illinois",
"KY" -> "Kentucky"
)

Add

Now you can add a single element to the Map with +=, like this:

1
states += ("AL" -> "Alabama")

You also add multiple elements using +=:

1
states += ("AR" -> "Arkansas", "AZ" -> "Arizona")

You can add elements from another Map using ++=:

1
states ++= Map("CA" -> "California", "CO" -> "Colorado")

D

You remove elements from a Map using -= and --= and specifying the key values, as shown in the following examples:

1
2
3
states -= "AR"
states -= ("AL", "AZ")
states --= List("AL", "AZ")

U

You update Map elements by reassigning their key to a new value:

1
states("AK") = "Alaska, A Really Big State"

Traversing a Map

a nice way to loop over all of the map elements is with this for loop syntax:

1
2
3
for ((k,v) <- ratings) println(s"key: $k, value: $v")

ratings.contains(element)

Set

The Scala Set class is an iterable collection with no duplicate elements.

Scala has several more Set classes, including SortedSet, LinkedHashSet, and more. Please see the Set class documentation for more details on those classes.

Tuple

元组是一个简洁的类,提供存储异构数据的方法。元组不可迭代。

1
val t = (3, "Three", new Person("Al"))

访问只能使用 ._id 的方式访问,其中 id 从 1 开始

1
2
3
4
5
6
7
8
scala> var t = (1, 2, 3, 1.1)
var t: (Int, Int, Int, Double) = (1,2,3,1.1)

scala> t._1
val res2: Int = 1

scala> t._4
val res3: Double = 1.1

可以使用类似 python golang 的初始化方式:

1
scala> val(x, y, z) = (3, "Three", new Person("David"))

Iterator

1
2
3
4
5
6
7
8
9
object Test {
def main(args: Array[String]) {
val it = Iterator("Baidu", "Google", "Runoob", "Taobao")

while (it.hasNext){
println(it.next())
}
}
}

SBT & SCALATEST

SBT

SBT常用命令

Directory structure

1
2
3
4
5
6
7
8
9
10
11
12
build.sbt
project/
src/
-- main/
|-- java/
|-- resources/
|-- scala/
|-- test/
|-- java/
|-- resources/
|-- scala/
target/

 编译项目:

  • 可以使用 sbt run 来构建项目
  • 也可以使用 sbt 交互式的构建项目
  • sbt reload 拉取镜像
  • sbt package 打包成 jar 包

Scala test

ctrl + shift + t || command + shift + t 快速创建 test

ScalaTest TDD 风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package pizza

import org.scalatest.funsuite.AnyFunSuite

class Tests extends AnyFunSuite {

// test 1
test("the name is set correctly in constructor") {
assert(true)
}

// test 2
test("test name") {
assert(true)
}

}

然后使用 sbt test 指令就可以运行了

ScalaTest BDD 风格

1
2
3
4
5
6
7
8
9
10
11
12
13
package simpletest

import org.scalatest.funspec.AnyFunSpec

class MathUtilsTest extends AnyFunSpec {
describe("test") {
it("should has zero test") {
val res = 0 * 2
assert(res == 0)
}
it("pending")(pending)
}
}
  • BDD 使用 AnyFunSpec ,TDD 使用 AnyFunSuite
  • 使用 pending 来标记还没有编写的 test

Functional Programming

Functional programming is a style of programming that emphasizes writing applications using only pure functions and immutable values.

pure function

  1. 函数的 output 取决于 input
  2. 函数不会改变任何隐藏状态,例如 verilog 的寄存器
  3. 函数不从外界读取和输出信息
  4. 仅存在副作用,任何返回Unit的函数都是副作用的

任何时候你用相同的输入值调用一个纯函数,你总是会得到相同的结果。

  • 纯函数:scala.math.abs

  • 不纯函数:集合类上的 foreach,调用了 STDOUT

FP applications have a core of pure functions combined with other impure functions to interact with the outside world.

处理空值

类似于 golang 返回 (String, error) 一样,但是 scala 没有这种处理逻辑,所以引入了 Option Some None

1
2
3
4
5
6
7
def toInt(s: String): Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
}

处理异常:

使用 match

1
2
3
4
toInt(x) match {
case Some(i) => println(i)
case None => println("That didn't work.")
}