Simple Scala: Type Less, Do More

分号

分号在scala中是可选的,scala编译器能够自动推断语句的结束,所以不必每行语句都添加,当然可以用在同一行写多条语句的情况.

// 行尾有等号时表示还有更多的语句
def equalSign(s:String) = 
    println("Another line.")

// 行尾有一个开放的大括号表示下一行有更多的代码
def equalSign2(s:String) = {
  println("Another line.")
}

// 行尾有逗号,句号或操作符均表示下一行有更多代码
def commas(s1:String,
            s2:String) = Console.
    println("comma: " + s1 +
        " , " + s2)

变量声明

scala允许你在声明变量时指认其为不可变(immutable)或可变(mutable)变量.顾名思义,不可变变量在声明后,其值就不能再做改变了,可变变量则可以.无论何时,声明后的变量类型不可改变.

// 声明一个不可变Array
scala> val array:Array[String] = new Array(5)          // 声明一个包含5String元素的不可变数组
array: Array[String] = Array(null, null, null, null, null)

scala> array = new Array(2)                            // 把array变量声明为一个新的Aarray,错误
<console>:8: error: reassignment to val
            array = new Array(2)

scala> array(0) = "Hello"                               // 初始化元素的值
scala> array
res1: Array[String] = Array(Hello, null, null, null, null)

一个变量被声明后必须被初始化.

// 声明一个可变的Double
scala> var stockPrice: Double = 100.0                   // 声明一个可变的Double类型变量 
stockPrice: Double = 100.0

scala> stockPrice = 200.0                               // 修改变量stockPrice的值
stockPrice: Double = 200.0

同时,在scala中的types, char, byte, short, int, long, float, double, boolean这些基本类型都是包含方法的对象,类似于引用类型.

当变量作为类的构造参数时,声明为val或var后,该变量则对应为该类的不可变或可变属性:

scala> class Person(val name: String, var age: Int) 
defined class Person

scala> class Person(val name: String, var age: Int) 
defined class Person

scala> p.name                                           // Show the value of firstName.
res0: String = Dean Wampler

scala> p.age                                            // Show the value of age.
res2: Int = 29 

scala> p.name = "Buck Trends"                           // 不允许修改声明为val的参数
<console>:9: error: reassignment to val
            p.name = "Buck Trends" ^

scala> p.age = 30
p.age: Int = 30                                         // 允许修改声明为var的参数

尽量使用immutable类型的参数可以最大限度的消除因为参数可变性造成的bug.

Ranges

Int, Long, Float, Double, Char, BigInt类型的随机数生成:

1 to 10
Range(1,2,3,4,5,6,7,8,9,10)

1 until 10
Range(1, 2, 3, 4, 5, 6, 7, 8, 9)

1 to 10 by 3
Range(1, 4, 7, 10)

10 to 1 by -3
1L to 10L by 3

1.1f to 10.3f by 3.1f
NumericRange(1.1, 4.2, 7.2999997)

1.1f to 10.3f by 0.5f
NumericRange(1.1, 1.6, 2.1, 2.6, 3.1, 3.6, 4.1, 4.6, 5.1, 5.6, 6.1, 6.6, 7.1, 7.6, 8.1, 8.6, 9.1, 9.6, 10.1)

1.1 to 10.3 by 3.1
NumericRange(1.1, 4.2, 7.300000000000001)

'a' to 'g' by 3
NumericRange(a, d, g)

BigInt(1) to BigInt(10) by 3
NumericRange(1, 4, 7, 10)

BigDecimal(1.1) to BigDecimal(10.3) by 3.1
NumericRange(1.1, 4.2, 7.3)

偏函数

偏函数是声明为 PartialFunction[-A,+B] 类型的一种函数,其中’A’是函数接受的参数类型,’B’是函数返回值类型.偏函数最大的特点就是只接受和处理其参数类型子集参数类型,而对于这个类型子集之外的参数抛出运行时异常.

这与模式匹配中的case语句语义相近,在模式匹配时,通常是定义一组具体的模式,然后使用’_’处理所有为定义的模式.如果一组case语句没有涵盖所有情况,则这个case语句就可以被视为一个偏函数.

val second:PartialFunction[List[Int],Int] = {  
    case List(x::y::_) => y  
}  

这个函数接受一个List[Int]类型的参数,返回一个Int类型的结果,这个结果是这个List参数的第二个元素.但是如果传入的List只有一个元素,就会抛出运行时异常.
如果在调用偏函数前想要检查一下参数是否在其定义域范围内,可以调用偏函数的isDefinedAt()方法:

scala> second.isDefinedAt(List(2,3,4))  
res0: Boolean = true  

scala> second.isDefinedAt(List(2))  
res1: Boolean = false

事实上,scala编译器把偏函数second编译成了:

new PartialFunction[List[Int], Int] {  
    def apply(xs: List[Int]) = xs match {  
        case x :: y :: _ => y  
    }  
    def isDefinedAt(xs: List[Int]) = xs match {  
        case x :: y :: _ => true  
        case _ => false  
    }  
} 

这种行为是编译期行为,我们必须显示的声明函数为PartialFunction类型,则编译器会把该函数编译成完整的普通函数.

因此,想要把一组case语句声明为一个偏函数,必须显式的声明其为PartialFunction类型.当确认程序不会传入不可处理的值时,就可以使用偏函数,这样一旦有定义域之外的值传入,程序会自动抛出运行时异常,而不需要手动编写抛出异常代码,节省代码量,只需处理异常即可.

方法声明

带默认值的方法和命名参数

定义一个Point类:

case class Point(x:Double = 0.0, y:Double = 0.0){           // 定义一个带有初始参数值的Point类
  def shift(deltax:Double = 0.0, deltay:Double = 0.0) =     // 定义一个带有默认参数值shift方法
    copy(x + deltax, y + deltay)
}

带有参数列表的方法

定义一个抽象类Shape:

abstract class Shape() { 
/**
 * Draw takes TWO argument LISTS, one list with an offset for drawing,
 * and the other list that is the function argument we used previously.
 */
    def draw(offset: Point = Point(0.0, 0.0))(f: String => Unit): Unit = 
    f(s"draw(offset = $offset), ${this.toString}")
}

Shape类的draw方法接受两个参数,第一个是Point类型的偏移值,并且提供了默认值Point(0.0, 0.0),即无偏移.第二个参数是一个函数,该函数接受一个String类型的参数并返回Unit.最终,draw方法返回Unit.
可以按如下方式调用该方法:

case class Circle(center: Point, radius: Double) extends Shape
case class Rectangle(lowerLeft: Point, height: Double, width: Double) extends Shape

s.draw(Point(1.0, 2.0))(str => println(s"ShapesDrawingActor: $str"))
s.draw(Point(1.0, 2.0)){str => println(s"ShapesDrawingActor: $str")}
s.draw(Point(1.0, 2.0)) { str => 
  println(s"ShapesDrawingActor: $str")
}
s.draw(Point(1.0, 2.0)) {
  str => println(s"ShapesDrawingActor: $str")
}
s.draw() {
  str => println(s"ShapesDrawingActor: $str")
}
s.draw(Point(1.0, 2.0),
  str => println(s"ShapesDrawingActor: $str")
)
s.draw(f = str => println(s"ShapesDrawingActor: $str"))

初试Futures

scala.concurrent.Future的API使用隐式转换参数来减少代码中的引用.是scala中提供的另外一种并发工具.Akka会使用Futures,但是如果你不需要使用完整的Akka,同样可以单独的使用Futures.
如果你再Future中包装一些动作,这些动作会以异步的方式执行,Future同时提供了多种方法来处理动作执行的结果,比如结果完成的时候提供回调方法:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global   // 提供隐式转换

def sleep(millis: Long) = { 
  Thread.sleep(millis)
}

// Busy work ;)
def doWork(index: Int) = { 
  sleep((math.random * 1000).toLong) 
  index
}

(1 to 5) foreach { index => 
  val future = Future {             // 使用Future封装函数执行动作
    doWork(index)       
  }
  future onSuccess {
    case answer: Int => println(s"Success! returned: $answer")
  }
  future onFailure {
    case th: Throwable => println(s"FAILURE! returned: $th") 
  }
}

sleep(1000) // Wait long enough for the "work" to finish.
println("Finito!")

嵌套函数定义和递归

使用嵌套函数定义一个阶乘计算器:

def factorial(i: Int): Long = {
  def fact(i: Int, accumulator: Int): Long = {
    if (i <= 1) accumulator else fact(i - 1, i * accumulator) 
  }

  fact(i, 1) 
}

(0 to 5) foreach ( i => println(factorial(i)) )

内部嵌套的函数不断的调用自己,这是一个递归函数,递归函数必须定义返回值类型,同时该函数只能在factorial函数内部可见.

在fact函数中调用fact(i - 1, i * accumulator)时,由于要保存之前”n”的值,必须分配一个新的函数栈,如果n值很大是,函数栈将很快被耗尽.尾递归则可以很好的处理这个问题,尾递归是指在函数处理的最后一步,只调用该递归函数本身,此时无需记录其他变量,当前的函数栈可以被重复利用,在效率上和循环是等价的.

import scala.annotation.tailrec

def factorial(i: Int): Long = { 
  @tailrec
  def fact(i: Int, accumulator: Int): Long = { 
    if (i <= 1) accumulator else fact(i - 1, i * accumulator)
  }

  fact(i, 1) 
}

(0 to 5) foreach ( i => println(factorial(i)) )

内部的fact函数要么返回结果,要么调用本身,变量accumulator每次递归调用都会更新值,直到边界条件满足是返回该值,即为最后的计算结果.注释 @tailrec可以确保写出的程序是正确的尾递归,否则编译时会报错误,以提醒修改代码.
JVM本身并不提供对尾递归的优化,java的编译器也不支持,只有scala的编译器提供.

字面值

各种基本类型的字面值与java类似.

Option,Some,and None:拒绝nulls

每种语言都会有一些关键字来引用那些没有任何值的变量类型,比如java中是null,是一个关键字而不是一个类型的实例,因此他不能调用任何方法,为什么会返回一个关键字而不是一个类型的实例,当需要一个实例时?
当一个值是null而不是任何实例的实例时,对其调用任何方法,则会报NullPointerExceptions,因此接到一个返回值时首先要检查是不是null然后再执行调用,但又如何判断不是null呢?

而scala中的Option可以很好的解决这个问题,它告诉你,返回值可能会没有意义.比如Option[String],指可能会返回一个String类型的值,也可能返回一个没有意义的值.Option有两个子类:Some和None,当返回Some的时候说明有一个正确的值,如果是None则是一个没有意义的值.

val stateCapitals = Map( 
    "Alabama" -> "Montgomery", 
    "Alaska" -> "Juneau",
    // ...
    "Wyoming" -> "Cheyenne"
)
stateCapitals.get("Alabama")                                res: Some(Montgomery)
stateCapitals.get("Wyoming")                                res: Some(Cheyenne)
stateCapitals.get("Unknown")                                res: None
stateCapitals.get("Wyoming").getOrElse("Oops!")             res: Some(Cheyenne)
stateCapitals.get("Unknown").getOrElse("Oops2!")            res: Some(Oops2!)

Map的get方法返回值类型为Option[T],本例中T为String.如果Option.get返回的结果是Some,则Some.get时返回Some中的值,但是如果Option.get的值是None的话,使用None.get会抛出一个 NoSuchElementException异常,而使用getOrElse则可以有效避免异常.

密封类

上面讨论的Option有一个很好的特性:只有两个有效的子类型Some和None.这是一种很好的设计方法,要么这样,否则那样,没有第三种情况出现.
scala中的关键字sealed可以实现这样的目标:

sealed abstract class Option[+A] ... { ... }

sealed关键字告诉编译器所有的子类必须在当前文件声明.Scala标准库中的Option的两个子类即是在Option类文件中实现.