Skip to content

dongma/scala-the-missing-tutorial

Repository files navigation

Scala: The Missing Tutorial

Scala是基于java virtual machine的一种语言,其广泛应用于诸多开源项目中,包括Apache Spark、Apache Kafka等。Martain Ordersky创造scala时,其运用了一些简洁的设计方法以及面向对象和函数式编程的一些看似简单却很强大的抽象,这使得Scala具备高聚合性。Scala是一门真正具备可扩展性的语言,我们既能使用它编写各种脚本语言,也能使用它实现大规模企业应用和中间件。

Contributing to the Scala: The Missing Tutorial

Please contribute if you see an error or something that could be better! Raise an issue or send me a pull request to improve. Contributions of all kinds, including corrections, additions, improvements, and translations, are welcome!

关于scala语言一些比较好的资源,包括《scala权威指南》、RockTheJvm的课程以及新近了解的CatsCats-Effect库:

变量声明和执行流程控制

在scala中声明变量可使用valvar关键字,对于不可变的变量使用val进行声明(java中的final),对于可变引用使用var关键字,语句末尾不需使用;作为该语句结束的标志。

val array: Array[String] = new Array[String](5)
var stockPrice: Double = 100.0

方法声明使用def funcName(variable1: Type=defaultValue, variable2:Type=defaultValue) = {function body}这种方式,参数列表中的默认值是可选的,当不存在时可省略。

case class Point(x: Double = 0.0, y: Double = 0.0) {
  
  def shift(deltax: Double = 0.0, deltay: Double = 0.0) =
  copy(x + deltax, y + deltay)
}

scala中的偏函数PartialFunction:在偏函数中并不处理所有可能的输入,只处理那些与case语句匹配的函数输入。

val pf1: PartialFunction[Any, String] = {
  case s: String => "yes"
}
val pf2: PartialFunction[Any, String] = {
  case d: Double => "yes"
}
val pf = pf1 orElse pf2
// 10为int类型因其与便函数中的case语句不匹配,故不会输出到控制台
List("str", 3.14, 10) foreach {item => print(pf(item).toString + "")}

scala中的for推导式,breed <- dogBreeds为生成器表达式,其会将右边集合中逐个元素提取出来赋予breed变量,处理后结果可通过yield进行收集(类似于java8中 stream().map().collect(Collectors.toList()))。item @ breed <- dogBreeds会将每个元素赋予变量item,该变量可跨作用域在yield部分使用。

val dogBreeds = List("doberman", "yorkshire terrier", "dachshud", "scottish terrier", "greate dane",
                     "portuguese water dog")
val filteredBreeds = for {
  item @ breed <- dogBreeds
  if breed.contains("terrier") && breed.startsWith("yorkshire")
} yield breed

定义枚举类需继承Enumeration类,type BreedEnum = Value定义类型 之后用变量列表表示枚举变量值。 提取枚举值使用BreedEnum.values属性返回类型为集合 并通过filter对其值进行过滤,变量的惰性赋值可使用lazy关键字。

object BreedEnum extends Enumeration {
  type BreedEnum = Value
  val doberman = Value("Doberman Pinscher")
  val yorkie = Value("Yorkshine Terrier")
  val scottie = Value("Scottish Terrier")
  val dane = Value("Great Dane")
  val portie = Value("Portuguese Water Dog")
}
// 通过自定义filter对BreedEnum.values中的数据进行过滤
BreedEnum.values filter (_.toString.endsWith("Terrier")) foreach println

lazy val resource: Int= init()

scala中异常捕获的语法,使用try..catch..finally的语法处理异常行为,其与java中的异常处理语法类似。catch语句中NonFatal(ex)用于捕获所有非致命性异常,finally语句块多用于数据库、文件资源的释放。

// 由于将source类声明为Option类型,因此我们在finally子句中能分辨出source对象是否是实例化
var source: Option[Source] = None
try {
  source = Some(Source.fromFile(filename))
  val size = source.get.getLines().size
  println(s"file $filename has $size lines")
} catch {
  // scala捕获异常的方式更为紧凑,case NonFatal(ex)捕获所有非致命性异常
  case NonFatal(ex) => println(s"Non fatal exception! $ex")
} finally {
  // <-操作实际用于从Option中获取value值,若source类型为None则不会发生任何事情
  for (s <- source) {
    println(s"Closing $filename..")
    s.close()
  }
}

match模式匹配与隐式转换

scala中的模式匹配与java或者C语言中的case语句很相似,都为根据变量值寻求匹配的case条件并执行相应语句,case分支需覆盖数值的所有条件。scala中的case语句可匹配数值、变量、类型、序列seq、元素及case类的匹配。

for {
  x <- Seq(1, 2, 2.7, "one", "two", "four")
} {
  val value = x match {
    case 1 => "int 1"
    case i: Int => "other int: " + i
    case d: Double => "a double: " + d
    case "one" => "string one"
    case s: String => "other string: " + s
    case unexpected => "unexpected value: " + unexpected
  }
	println(value)
}

简单的case语句会使用变量xcase条件分支进行比较,case 1case i:Intcase d: Doublecase "one"会将x变量的类型及数值进行比较,若匹配则执行子表达式=>之后的语句。

// case类的匹配:可以在case语句中根据Person类成员字段属性值进行匹配
val alice = Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA"))
val bob = Person("Bob", 29, Address("2 Java Ave", "Miami", "USA"))
val charlie = Person("Charlie", 32, Address("3 Python Ct", "Boston", "USA"))
for (person <- Seq(alice, bob, charlie)) {
  person match {
    // p@...语法会将整个Person类的实例赋值给p, 当不需要从p的实例中取值只需写 p : Person => 就可以
    case p @ Person("Alice", 25, Address(_, "Chicago", _)) => println("Hi Alice! $p")
    case p @ Person("Bob", 29, a @ Address("2 Java Ave", "Miami", "USA")) =>
    println(s"hi ${p.name}! age {${p.age}}, in ${a.city}")
    case p @ Person(name, age, _) => println(s"Who are you, $age year-old person and $name? $p")
  }
}

case类的匹配与简单case语句匹配类似,在match表达式中其会对变量person进行匹配(类型和数值)。Person("Alice", 25, Address(_, "Chicago", _))中匹配personnameAliceage25 ,并且居住地在Chicago的变量。可使用case p @ Person case表达式中匹配的变量进行绑定,在匹配表达中通过$p访问变量信息。

case p @ Person("Alice", 25, Address(_, "Chicago", _)) => println("Hi Alice! $p")

case类有一个伴随对象,伴随对象有一个名为apply的工厂方法,用于构造对象。可以推断一定还存在另一个自动生成的unapply方法,用于提取和解构对象。case类匹配时会将Address变量中的属性streetcountry进行抽取。

scala中隐式转换implicit的用法,使用隐式能够减少代码、能够向已有类型中注入新的方法(implicit method)。calcTax函数有两个参数列表(amount: Float)和隐式(implicit rate: Float)。使用${calcTax(amount)}调用calcTax函数,由于只传递了一个参数,scala则会从当前作用域中寻找类型匹配implicit变量SimpleStateSalesTax.rate作为参数。

def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate
object SimpleStateSalesTax {
  implicit val rate: Float = 0.05F
}
{
  import SimpleStateSalesTax.rate
  val amount = 100F
  println(s"Tax on $amount = ${calcTax(amount)}")
}

scala中的集合操作

不同语言有不同的核心数据结构,但大致都包含同一个子集,子集中包含列表(list)、向量(vector)等序列型集合,数组(array)、映射(map)和集合(set)。每种都支持一批无副作用的高阶函数,称为组合器(combinar),如mapfilterfold等函数。

scala中使用::操作符向列表中追加元素,该元素会被追加到列表的头部,成为新列表的"头部"。除了头部,剩下的部分就是原列表中的元素,这些元素都没有被修改。Nilscala中代表空队列,可以将Nil元素放到Seq的最后。List中还定义了:++:操作符,:+用于在尾部追加元素、+:方法用于在头部追加元素。

// scala中::向队列头部追加数据,从而创建新的列表. scala中以:结尾的方法向右结合,因此x::list调用其实是list.::(x),
// list2的计算结果如下: list1.::("read").::("should").::("People")
val list2 = "People" :: "should" :: "read" :: list1
// - all elements in list2 use :: operator are: [List(People, should, read, programming, scala)]
logger.info("all elements in list2 use :: operator are: [{}]", list2)

// 使用++能将两个list拼接起来, 拼接顺序与list的顺序相同 (将两个list->flatmap->整合元素)
val list4 = Seq("people", "should", "read") ++ list1

scala中对于map的操作,key -> value的形式语法形式实际上是用库中隐式转换实现的,实际上是调用了Map.apply方法。向map中添加元素可使用+操作符,移除元素使用-后跟mapkey的列表(stateCapitals2 - "Wyoming" - "Alabama")。

val stateCapitals = Map("Alabama" -> "Montgomery", "Alaska" -> "Juneau", "Wyoming" -> "Cheyenne")
val lengths = stateCapitals map { entry => (entry._1, entry._2.length) }
logger.info("static value length in stateCapitals map are: {}", lengths)
// 在向Map中添加entry时 也可使用+操作符操作,新的entry元素会被添加到Map中
val stateCapitals2 = stateCapitals + ("Virginia" -> "Richmond")
logger.info("use + operator to add entry to Map, value: {}", stateCapitals2)
// 使用-操作符从Map中根据key移除"Wyoming"、"Alabama"元素列表
val elements = stateCapitals2 - "Wyoming" - "Alabama"
logger.info("remove Wyoming、Alabama from stateCapitals2 value: {}", elements)

scala中的OptionSomeNone操作符,其作用是对java中的null问题进行优化。注意:如果Option是一个SomeSome.get则会返回其中的值。然而,如果Option事实上是一个None值,None.get就会抛出一个NoSuchElementException的异常。一般使用时会用get的替代方案getOrElse获取数值。

// [main] INFO org.lang.scala.SeqOperate$ - Alabama: Some(Montgomery)
logger.info("Alabama: " + stateCapitals.get("Alabama"))

Option还用于在case表达式中核验数据是否存在,在spark中有用Some提取数据的用法。当Some判断存在数据时,执行replicatedVertexView.withActiveSet(activeSet)case Somecase None覆盖了取值的两种方式。

val view = activeSetOpt match {
  case Some((activeSet, _)) =>
  replicatedVertexView.withActiveSet(activeSet)
  case None =>
  replicatedVertexView
}
val activeDirectionOpt = activeSetOpt.map(_._2)

Releases

No releases published

Packages

No packages published

Languages