Clojure首先是FP, 但是由於基於JVM, 所以不得已需要做出一些妥協, 包含一些OO的編程方式
Scala首先是OO, Java語法過於冗余, 一種比較平庸的語言, Scala首先做的是簡化, 以更為簡潔的方式來編寫OO, 主要利用‘type inference’能推斷出來的, 你就不用寫, 但如果僅僅這樣, 不如用python
所以Scala象其名字一樣, “可伸展的語言”, 它是個大的集市, 它積極吸納其他語言的優秀的特征, 最重要的就是FP, 你可以使用Scala來寫OO, 但它推薦使用FP的方式來寫Scala; 還包括Erlang里面的actor模型
所以Scala並不容易學, 因為比較繁雜
我主要對Scala programing做個筆記, 下面這個哥們整理的更好
http://qiujj.com/static/Scala-Handbook.htm
http://docs.scala-lang.org/ , Scala官方文檔
http://www.scala-lang.org/api/current/#package, 參考手冊
http://twitter.github.io/effectivescala/index-cn.html, Effective Scala
0
Scala interpreter
scala> 1 + 2
res0: Int = 3
scala> res0 * 3
res1: Int = 9
scala> println("Hello, world!")
Hello, world!
代碼段落
scala中;常常可以省略, 但如果一行有多條語句, 則必須加上
val s = "hello"; println(s)
如果一條語句, 要用多行寫
x
+ y
這樣會當作2個語句, 兩種方法解決,
(x
+ y) //加括號x +
y +
z //把操作符寫在前一行, 暗示這句沒完
1 基本語法
基本類型
Rich wrappers, 為基本類型提供更多的操作
變量定義
val, 不可變變量, 常量, 適用於FP
var, 可變變量, 適用於OO
scala> val msg = "Hello, world!" msg: java.lang.String = Hello, world! scala> msg = "Goodbye cruel world!" <console>:5: error: reassignment to val msg = "Goodbye cruel world!"
函數定義
可以簡寫為, 返回值類型不需要寫, 可以推斷出, 只有一條語句, 所以{}可以省略
scala> def max2(x: Int, y: Int) = if (x > y) x else y max2: (Int,Int)Int
簡單的funciton, 返回值為Unit, 類似Void(區別在於void為無返回值, 而scala都有返回值, 只是返回的為Unit, ())
scala> def greet() = println("Hello, world!")
greet: ()Unit
scala> greet() == ()
Boolean = true
函數參數不可變
def add(b: Byte): Unit = {
b = 1 // This won’t compile, because b is a val
sum += b
}
重復參數, 最后的*表示可變參數列表, 可傳入多個string
scala> def echo(args: String*) = for (arg <- args) println(arg)
scala> echo("hello", "world!")
hello
world!
Function literal
如何翻譯...
Scala FP的基礎, function作為first class, 以function literal的形式作為參數被傳遞
args.foreach((arg: String) => println(arg))
args.foreach(arg => println(arg)) //省略類型
args.foreach(println) //其實連參賽列表也可以省略
可以看到scala在省略代碼量上可以說下足功夫, 只要能推斷出來的你都可以不寫, 這也是對於靜態類型系統的一種形式的彌補
對於oo程序員, 可能比較難理解, 其實等於
for (arg <args)
println(arg)
控制結構
由於scala是偏向於FP的, 所以所有控制結構都有返回值, 這樣便於FP編程
If, 可以返回值
val filename = if (!args.isEmpty) args(0) else "default.txt"
While, 在FP里面不推薦使用循環, 應該用遞歸,盡量避免
For, 沒有python和clojure的好用或簡潔
for ( file <- filesHere //generator,用於遍歷,每次file都會被從新初始化 if file.isFile; //過濾條件, 多個間需要用; if file.getName.endsWith(".scala"); //第二個過濾 line <- fileLines(file) //嵌套for trimmed = line.trim //Mid-stream variable bindings, val類型,類似clojure let if trimmed.matches(pattern) ) println(file +": "+ trimmed)
//for默認不會產生新的集合, 必須使用yield def scalaFiles = for { file <- filesHere if file.getName.endsWith(".scala") } yield file //yield產生新的集合,類似python
match, switch-case
可以返回值, FP風格, 這樣只需要最后println一次
默認會break, 不需要每次自己加
val firstArg = if (!args.isEmpty) args(0) else "" val friend = firstArg match { case "salt" => "pepper" case "chips" => "salsa" case "eggs" => "bacon" case _ => "huh?" //default } println(friend)
Option and Either
參考, http://www.ibm.com/developerworks/cn/java/j-ft13/index.html
Option和Either都是用來讓返回值可以有兩個選擇
而Option是比較簡單的版本, 兩個選擇, 一定是成功Some, 和失敗None
Option意味着可能有值some(x), 也可能沒有值(用None對象, 表示缺失), 典型的例子就是從字典里取值
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo") def show(x: Option[String]) = x match { //Option類型, 可選的String case Some(s) => s case None => "?" } scala> show(capitals get "France") res24: String = Paris scala> show(capitals get "North Pole") res25: String = ?
以前的方式, 比如Java, 通過null來表示沒有取到值, 但是有的時候null可能作為合法值出現, 就需要特殊處理, 很麻煩
而Scala提供option來比較優雅的解決這個問題
Either, 更為通用一些, 可用自己定義兩種選擇, 直接看個spark源碼中的例子,
對於PutResult中的data, 有可能是ByteBuffer或者Iterator
而使用的時候, 使用Left和Right來選擇到底用哪一個
private[spark] case class PutResult(size: Long, data: Either[Iterator[_], ByteBuffer])
PutResult(sizeEstimate, Left(values.iterator)) PutResult(bytes.limit(), Right(bytes.duplicate()))
這里無論option或either都提高了極好的靈活性, 在Java中如果要返回一個有兩種可能性的值就比較不那么優雅了, 參考上面的鏈接
Implicit Conversions, 隱式轉換
Scala支持這種隱式轉換,
implicit def intToString(x: Int) = x.toString //定義轉換函數后, 編譯器會自動做轉換
比如x op y無法通過類型檢查, int x 不支持op操作, 而string支持, 編譯器就會自動調用上面的轉換函數, 將int x轉化為string x
隱式參數
用法也比較詭異, 看例子
object Greeter { def greet(name: String)(implicit prompt: PreferredPrompt) {//聲明2個參數, 第二個是可以隱式的,當然你也可以顯式的寫 println("Welcome, "+ name +". The system is ready.") println(prompt.preference) } } object JoesPrefs { implicit val prompt = new PreferredPrompt("Yes, master> ") //聲明成implicit, 可用作補充 } import JoesPrefs._ scala> Greeter.greet("Joe") //編譯器會自動補充成, greet("Joe")(prompt) Welcome, Joe. The system is ready. Yes, master>
2 數據結構
數組
可變的同類對象序列, 適用於OO場景
val greetStrings = new Array[String](3) //greetStrings為val, 但是內部的數組值是可變的
greetStrings(0) = "Hello" //scala用()而非[]
greetStrings(1) = ", "
greetStrings(2) = "world!\n"
for (i <- 0 to 2)
print(greetStrings(i))
Scala 操作符等價於方法, 所以任意方法都可以以操作符的形式使用
1 + 2 //(1).+(2), 在只有一個參數的情況下, 可以省略.和()
0 to 2 //(0).to(2)
greetStrings(0) //greetStrings.apply(0),這也是為什么scala使用(), 而非[]
greetStrings(0) = "Hello" //greetStrings.update(0, "Hello")
簡化的array初始化
val numNames = Array("zero", "one", "two") //Array.apply("zero", "one", "two")
List
相對於array, List為不可變對象序列, 適用於FP場景
val oneTwo = List(1, 2)
val threeFour = List(3, 4)val zeroOneTwo = 0 :: oneTwo //::
val oneTwoThreeFour = oneTwo ::: threeFour
對於List最常用的操作符為::, cons, 把新的elem放到list最前端
:::, 兩個list的合並
右操作數, ::
普通情況下, 都是左操作數, 比如, a * b => a.*(b)
但是當方法名為:結尾時, 為右操作數
1 :: twoThree => twoThree.::(1)
不支持append
原因是, 這個操作的耗時會隨着list的長度變長而線性增長, 所以不支持, 只支持前端cons, 實在需要append可以考慮ListBuffer
支持模式
scala> val List(a, b, c) = fruit a: String = apples b: String = oranges c: String = pears scala> val a :: b :: rest = fruit a: String = apples b: String = oranges rest: List[String] = List(pears)
list zip,嚙合
scala> abcde.indices zip abcde //indices返回所有有效索引值 res14: List[(Int, Char)] = List((0,a), (1,b), (2,c), (3,d),(4,e)) scala> val zipped = abcde zip List(1, 2, 3) zipped: List[(Char, Int)] = List((a,1), (b,2), (c,3)) //會自動截斷
Folding lists: /: and :\
折疊操作, 比較奇怪的操作, 看例子
(z /: List(a, b, c)) (op) equals op(op(op(z, a), b), c) scala> def sum(xs: List[Int]): Int = (0 /: xs) (_ + _) //實現列表元素疊加 (List(a, b, c) :\ z) (op) equals op(a, op(b, op(c, z))) def flattenLeft[T](xss: List[List[T]]) = (List[T]() /: xss) (_ ::: _) //兩種的區別 def flattenRight[T](xss: List[List[T]]) = (xss :\ List[T]()) (_ ::: _)
Queues
import scala.collection.immutable.Queue //不可變Queue
val empty = new Queue[Int] val has1 = empty.enqueue(1) //添加單個元素 val has123 = has1.enqueue(List(2, 3)) //添加多個元素 val (element, has23) = has123.dequeue //取出頭元素,返回兩個值, 頭元素和剩下的queue element: Int = 1 has23: scala.collection.immutable.Queue[Int] = Queue(2,3)
import scala.collection.mutable.Queue //可變Queue val queue = new Queue[String] queue += "a" //添加單個 queue ++= List("b", "c") //添加多個 queue.dequeue //取出頭元素, 只返回一個值 res22: String = a scala> queue res23: scala.collection.mutable.Queue[String] = Queue(b, c)
Stacks
import scala.collection.mutable.Stack val stack = new Stack[Int] stack.push(1) stack.push(2) scala> stack.top res8: Int = 2 scala> stack.pop res10: Int = 2 scala> stack res11: scala.collection.mutable.Stack[Int] = Stack(1)
Tuple
tuple和list一樣是不可變的, 不同是, list中的elem必須是同一種類型, 但tuple中可以包含不同類型的elem
val pair = (99, "Luftballons") //自動推斷出類型為,Tuple2[Int, String] println(pair._1) //從1開始,而不是0,依照Haskell and ML的傳統 println(pair._2) //elem訪問方式不同於list, 由於元組中elem類型不同
Set和Map
var jetSet = Set("Boeing", "Airbus")
jetSet += "Lear"
println(jetSet.contains("Cessna"))
val treasureMap = Map[Int, String]() treasureMap += (1 -> "Go to island.") treasureMap += (2 -> "Find big X on ground.") println(treasureMap(2))
val romanNumeral = Map(1 -> "I", 2 -> "II", 3 -> "III" ) //簡寫
Scala需要兼顧OO和FP, 所以需要提供mutable和immutable版本
這里默認是Immutable, 如果需要使用mutable版本, 需要在使用前顯示的引用...
import scala.collection.mutable.Set
import scala.collection.mutable.Map
Sorted Set and Map
和Java一樣, Scala也提供基於紅黑樹實現的有序的Set和Map
import scala.collection.immutable.TreeSet scala> val ts = TreeSet(9, 3, 1, 8, 0, 2, 7, 4, 6, 5) ts: scala.collection.immutable.SortedSet[Int] = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) scala> val cs = TreeSet('f', 'u', 'n') cs: scala.collection.immutable.SortedSet[Char] = Set(f, n, u) import scala.collection.immutable.TreeMap scala> var tm = TreeMap(3 ->'x', 1 ->'x', 4 ->'x') scala> tm += (2 ->'x') scala> tm res38: scala.collection.immutable.SortedMap[Int,Char] = Map(1 ->x, 2 ->x, 3 ->x, 4 ->x)
3 面向對象-OO
類和對象
相對於Java定義比較簡單, 默認public
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = {
sum += b
}def checksum(): Int = {
return ~(sum & 0xFF) + 1
}
}
不寫return是推薦的方式, 因為函數盡量不要有多個出口
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = sum += b
def checksum(): Int = ~(sum & 0xFF) + 1
}
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte) { sum += b } //對於Unit返回的, 另一種簡寫, 用{}來表示無返回, 所以前面的就不用寫了
def checksum(): Int = ~(sum & 0xFF) + 1
}
實例化
val acc = new ChecksumAccumulator
val csa = new ChecksumAccumulator
acc.sum = 3
Singleton對象
Scala不能定義靜態成員, 所以用Singleton對象來達到同樣的目的
import scala.collection.mutable.Map object ChecksumAccumulator { //用object代替class private val cache = Map[String, Int]() def calculate(s: String): Int = if (cache.contains(s)) cache(s) else { val acc = new ChecksumAccumulator for (c <s) acc.add(c.toByte) val cs = acc.checksum() cache += (s -> cs) cs } }

最常見的場景就是, 作為scala程序的入口,
To run a Scala program, you must supply the name of a standalone singleton object with a main method that takes one parameter(Array[String]), and has a result type of Unit.
import ChecksumAccumulator.calculate object Summer { def main(args: Array[String]) { for (arg <args) println(arg +": "+ calculate(arg)) } }
不可變對象
適用於FP場景的對象, 所以也叫做Functional Objects.
好處, 消除可變帶來的復雜性, 可以放心的當參數傳遞, 多線程下使用啊...
下面以定義有理數類為例
class Rational(n: Int, d: Int) //極簡方式,沒有類主體
和上面的定義比, 不需定義成員變量, 而只是通過參數, 因為根本沒有定義成員變量, 所以無處可變.
class Rational(n: Int, d: Int) { require(d != 0) //Precondition, 如果require返回false會拋出IllegalArgumentException,阻止初始化 private val g = gcd(n.abs, d.abs) val numer = n / g //添加不可變成員字段,便於引用 val denom = d / g def this(n: Int) = this(n, 1) //輔助構造函數 def + (that: Rational): Rational = //定義操作符 new Rational( numer * that.denom + that.numer * denom, denom * that.denom //也可以使用this.number引用成員 ) def + (i: Int): Rational = //典型的成員函數重載 new Rational(numer + i * denom, denom) override def toString = numer +"/"+ denom //override, 方法重載 private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) }
可變對象
可變Objects本身沒啥好說的, 說說Scala getter和setter規則
every var that is a non-private member of some object implicitly defines a getter and a setter method with it.
The getter of a var x is just named “x”, while its setter is named “x_=”.
每個非私有的var都會隱含的自動定義getter和setter, 如下面的例子
class Time { var hour = 12 var minute = 0 } //等同於 class Time { private[this] var h = 12 private[this] var m = 0 def hour: Int = h def hour_=(x: Int) { h = x } def minute: Int = m def minute_=(x: Int) { m = x } }
所以在Scala中比較有趣的是, 其實你可以不真正定義這個成員, 而只需要定義getter和setter就可以
如下面的例子, 並沒有真正定義華氏溫度, 而只是定義了getter和setter, 更簡潔
class Thermometer { var celsius: Float = _ def fahrenheit = celsius * 9 / 5 + 32 def fahrenheit_= (f: Float) { celsius = (f 32) * 5 / 9 } override def toString = fahrenheit +"F/"+ celsius +"C" }
組合和繼承
抽象類
abstract class Element { //abstract表示抽象類 def contents: Array[String] //沒有函數體表示聲明抽象方法 }
無參函數和屬性, 統一訪問模式
客戶可以用同樣的方式來訪問函數width或屬性width, 這樣的好處是width改變不同的實現方式時, 不會影響客戶代碼, 實現統一訪問模式
函數和屬性的區別, 函數節省空間而需要每次計算, 屬性是初始化時算好的, 比較快, 但需要額外的空間, 所以可用在不同場景下選擇不同實現
abstract class Element { def contents: Array[String] //組合Array[] def height: Int = contents.length //無參函數 def width: Int = if (height == 0) 0 else contents(0).length } //等同於 abstract class Element { def contents: Array[String] val height = contents.length //屬性 val width = if (height == 0) 0 else contents(0).length }
Scala為了實現這種統一的訪問模式, 一般對於無參函數在調用時可以省掉(), 但前提是該無參函數是沒有副作用的, 否則就會比較confusing
"hello".length // no () because no sideeffect println() // better to not drop the (),因為有副作用
繼承
超類的私有成員不會被子類繼承.
抽象成員, 需要被子類實現(implement)
一般成員, 可用被子類重寫(override)
override關鍵字的使用,
重寫時, 必須顯式加override, 防止意外的重寫
實現時, 是否寫override可選
其他情況禁止使用
class ArrayElement(conts: Array[String]) extends Element { def contents: Array[String] = conts //實現抽象函數 }
這個圖, 反映出繼承和組合, 其中Element繼承自AnyRef, 而ArrayElement組合Array[]
重寫方法和屬性
這是scala比較特別的地方, 方法和屬性在同一個命名空間, 所以方法和屬性不能同名
並且支持, 在override時, 方法和屬性間可用互相轉換
前面將contents聲明成方法, 是否覺得有些別扭, 看上去更像屬性, 是的, 你可用在子類中實現或重寫成屬性
在scala中, 無副作用的無參函數和屬性是可以簡單的互相轉換
class ArrayElement(conts: Array[String]) extends Element { val contents: Array[String] = conts //將方法contents實現成屬性 }
Java和Scala的命名空間對比
Java’s four namespaces are fields, methods, types, and packages.
By contrast, Scala’s two namespaces are:
• values (fields, methods, packages, and singleton objects)
• types (class and trait names)
參數化屬性
一種scala的簡化,
是否覺得, 定義參數, 再將參數賦值給屬性, 很麻煩, ok, scala可用簡化
class ArrayElement( val contents: Array[String] ) extends Element //實現屬性
另一個例子,
class Cat { val dangerous = false } class Tiger( override val dangerous: Boolean, //重寫屬性 private var age: Int //增加新的屬性 ) extends Cat
超類構造器
和C++或Pythno一樣, 需要在子類中顯式的構造超類, 比如這里需要先構造ArrayElement
只不過Scala提供一種簡寫的方式, 而不用在代碼中另寫一行
class LineElement(s: String) extends ArrayElement(Array(s)) { override def width = s.length override def height = 1 }
final成員
class ArrayElement extends Element { final override def demo() { //該成員函數禁止被任何子類override println("ArrayElement's implementation invoked") } } final class ArrayElement extends Element {//ArrayElement禁止被任何子類繼承 override def demo() { println("ArrayElement's implementation invoked") } }
工廠對象實現
object Element { //用伴生對象(Singleton)來做工廠對象 def elem(contents: Array[String]): Element = new ArrayElement(contents) def elem(chr: Char, width: Int, height: Int): Element = new UniformElement(chr, width, height) def elem(line: String): Element = new LineElement(line) }
Scala的類層次
At the top of the hierarchy is class Any.
Any聲明如下接口, 故所有類都支持這些接口,
final def ==(that: Any): Boolean final def !=(that: Any): Boolean def equals(that: Any): Boolean def hashCode: Int def toString: String
Any有兩個子類, AnyVal和AnyRef
AnyVal是所有built-in value class的父類, 除了常見的基本類型外, 還有Unit
AnyRef是所有reference classes的父類, 其實就是java.lang.Object
Scala還定義, Nothing和Null class
Nothing是所有Any的子類, 而Null是所有AnyRef的子類
干嗎用的?用於以統一的方式來處理邊界形式或異常返回, 因為他們是所有類的子類, 所有可用返回給任意返回類型
例子,
def error(message: String): Nothing = throw new RuntimeException(message) def divide(x: Int, y: Int): Int = //返回類型為Int if (y != 0) x / y else error("can't divide by zero") //異常時返回Nothing
Package管理
支持package嵌套和通過{}指定范圍
package launch { class Booster3 } package bobsrockets { package navigation { class Navigator } package launch { class Booster { // No need to say bobsrockets.navigation.Navigator val nav = new navigation.Navigator val booster3 = new _root_.launch.Booster3 //_root_特指頂層包 } } }
Import的用法
import bobsdelights.Fruit import bobsdelights._ //import all import Fruits.{Apple, Orange} //指定import import Fruits.{Apple => McIntosh, Orange} //別名 import Fruits.{Apple => _, _} //排除Apple
訪問修飾符
Private
基本和Java一致,但是在對inner class上表現出更好的一致性
Java允許外部類訪問內部類的private成員,而scala不行,如下例子中的error case
class Outer { class Inner { private def f() { println("f") } class InnerMost { f() // OK } } (new Inner).f() // error: f is not accessible }
Protected
同樣比Java更嚴格一些,僅允許在子類中被訪問
而Java允許同一個包中的其他類訪問該類的protected成員,而scala中不行,如下面的error case
package p { class Super { protected def f() { println("f") } } class Sub extends Super { f() } class Other { (new Super).f() // error: f is not accessible } }
修飾符作用域
scala中的修飾符可以加上作用域,非常靈活
private[X] or protected[X] means that access is private or protected “up to” X, where X designates some enclosing package, class or singleton object.
加上作用域的意思,除了原來private和protect表示的可見范圍外,將可見范圍擴展到X,X可以是package,class,singleton對象
package bobsrockets { package navigation { private[bobsrockets] class Navigator { //使Navigator類在bobsrockets package中可見,所以在package launch中可以new Navigator protected[navigation] def useStarChart() {} //在Navigator和子類,以及package Navigation中可以被訪問(等同於Java) class LegOfJourney { private[Navigator] val distance = 100 //是私有變量distance,在外部類Navigtor中可見(等同於Java) } private[this] var speed = 200 //僅僅在相同對象中可以訪問,比private的相同類訪問還嚴格,如下例 val other = new Navigator other.speed // this line would not compile } } package launch { import navigation._ object Vehicle { private[launch] val guide = new Navigator } } }
伴生對象
A class shares all its access rights with its companion object and vice versa.
Trait
概念上類似Java的interface, 但是更強大, 可以有方法實現, 屬性...
其實幾乎等同於class, 除了兩點,
a. trait不能有任何參數
b. trait中super是動態綁定的, 因為定義的時候根本就不知道super是誰
trait Philosophical {
def philosophize() {
println("I consume memory, therefore I am!")
}
}
mix in有兩種方式, extend和with
extend, implicitly inherit the trait’s superclass
所以下面的例子, Frog會繼承Philosophical的超類AnyRef
class Frog extends Philosophical { //mix in Philosophical trait override def toString = "green" }
with, 需要顯式的指明超類
class Animal trait HasLegs class Frog extends Animal with Philosophical with HasLegs {//繼承Animal,並mix in兩個traits override def toString = "green" }
Trait真正的作用在於, 模塊封裝, 可以把相對獨立的功能封裝在trait中, 並在需要的時候進行mix in, 如下面的例子
//傳統的例子 class Rational(n: Int, d: Int) { // ... def < (that: Rational) = this.numer * that.denom > that.numer * this.denom def > (that: Rational) = that < this def <= (that: Rational) = (this < that) || (this == that) def >= (that: Rational) = (this > that) || (this == that) } //將比較接口和實現封裝在Ordered Trait中,這里僅僅需要mix in和實現compare class Rational(n: Int, d: Int) extends Ordered[Rational] { // ... def compare(that: Rational) = (this.numer * that.denom) ( that.numer * this.denom) }
Traits as stackable modifications
trait的強大還體現在可以同時mix in多個traits, 並以類似stackable形式的線性調用, 即多個trait會效果疊加
而傳統的多重繼承, 只有一個實現會有效
abstract class IntQueue { def get(): Int def put(x: Int) } import scala.collection.mutable.ArrayBuffer class BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x: Int) { buf += x } } trait Doubling extends IntQueue { //trait繼承自IntQueue, 說明該trait只能mix in IntQueue(或子類)中 //注意這里super的使用(動態綁定), 和abstract override(僅用於這種場景) //表示trait must be mixed into some class that has a concrete definition of the method in question abstract override def put(x: Int) { super.put(2 * x) } } trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) } } trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) } }
下面來看看, 怎么樣進行stackable的調用
//同時mix in了Incrementing和Filtering //注意調用順序是, 從右到左(stackable) //這個例子, 先調用Filtering, 再調用Incrementing, 順序不同會帶來不同的結果 scala> val queue = (new BasicIntQueue with Incrementing with Filtering) queue: BasicIntQueue with Incrementing with Filtering... scala> queue.put(-1); queue.put(0); queue.put(1) scala> queue.get() res15: Int = 1 scala> queue.get() res16: Int = 2
To trait, or not to trait
If the behavior will not be reused, then make it a concrete class.
If it might be reused in multiple, unrelated classes, make it a trait.
If you want to inherit from it in Java code, use an abstract class.
If you plan to distribute it in compiled form, and you expect outside groups to write classes inheriting from it, you might lean towards using an abstract class.
If efficiency is very important, lean towards using a class.
4 函數編程-FP
函數和閉包
成員函數, OO的方式
內部函數, 需要切分功能, 又不想污染外部的命名空間
First-class function, unnamed function literal
function象變量一樣, 可以被賦值和當參數傳遞, 但在scala需要以function literal的形式, 在運行期的時候會實例化為函數值(function value)
scala> var increase = (x: Int) => x + 1 scala> increase(10)
Partially applied functions
scala> def sum(a: Int, b: Int, c: Int) = a + b + c scala> val a = sum _ //用占位符代替整個參數列表 scala> a(1, 2, 3) //a.apply(1, 2, 3) res13: Int = 6 scala> val b = sum(1, _: Int, 3) //partial function, 用占位符代替一個參數 scala> b(2) res15: Int = 6
Closures
關於閉包的解釋,
對於通常的function, (x: Int) => x + 1, 稱為closed term
而對於(x: Int) => x + more, 稱為open term
所以對於開放的, 必須在定義的時候對里面的自由變量more動態進行綁定, 所以上下文中必須要有對more的定義, 這種關閉open term過程產生了closure
scala> var more = 1 scala> val addMore = (x: Int) => x + more //產生閉包,綁定more scala> addMore(10) res19: Int = 11 scala> more = 9999 scala> addMore(10) res21: Int = 10009 //可見閉包綁定的不是value,而是變量本身
剛看到有些驚訝, 去clojure里面試一下, 也是這樣的, 綁定的變量本身, 閉包會取最新的值
當然一般不會這樣使用閉包.
下面這個例子, 是較常用的case, 其中閉合了函數的參數
如何在閉包調用時, 可以訪問到已經不存在的變量? 當產生閉包時, 編譯器會將這個變量從堆棧放到堆里面, 所以函數結束后還能訪問
def makeIncreaser(more: Int) = (x: Int) => x + more
scala> val inc1 = makeIncreaser(1)
scala> val inc9999 = makeIncreaser(9999)
scala> inc1(10)
res24: Int = 11
scala> inc9999(10)
res25: Int = 10009
Currying
先提供sum的兩個版本的比較,
scala> def plainOldSum(x: Int, y: Int) = x + y
scala> plainOldSum(1, 2)
res4: Int = 3
scala> def curriedSum(x: Int)(y: Int) = x + y //currying版本的sum
curriedSum: (Int)(Int)Int
scala> curriedSum(1)(2)
res5: Int = 3
其實currying, 等同於調用兩次function, first會返回第二個函數的函數值, 其中closure了x
scala> def first(x: Int) = (y: Int) => x + y first: (Int)(Int) => Int
取出函數值, 效果是減少了參數個數, 第一個函數的參數已經closure在第二個函數中了, 和partially有些類似(區別)
scala> val onePlus = curriedSum(1)_ onePlus: (Int) => Int = <function> scala> onePlus(2) res7: Int = 3
有什么用? 用於創建更像built-in的控制結構
如下, 使用{}更像built-in, 但{}有個限制是, 只有單個參數的參數列表可以用{}替換(), 所以這個時候需要用currying來降低參賽個數
scala> println("Hello, world!") //象方法調用 scala> println { "Hello, world!" } //更像built-in的控制結構,比如if
對於FP, 相對於OO使用繼承和多態, 使用函數作為參數來實現代碼重用, 希望可以將函數值放在{}, 顯得更象built-in
比如下面, 每次打開文件, 操作, 關閉文件, 固定模式, 所以實現withPrintWriter, 每次傳入不同的op就可以進行不同的操作, 而不用考慮文件開關
如果是oo實現, 就需要傳入基類對象, 利用多態實現, 明顯使用函數更輕量級一些
def withPrintWriter(file: File, op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } } //以調用方法的方式使用 withPrintWriter( new File("date.txt"), writer => writer.println(new java.util.Date) )
通過currying減少了參數, 所以就可以使用{}
def withPrintWriter(file: File)(op: PrintWriter => Unit) {......} //currying版本 val file = new File("date.txt") withPrintWriter(file) { writer => writer.println(new java.util.Date) } //將函數值放在{}, 很像built-in
尾遞歸,Tail recursion
前面說了, FP盡量避免使用循環, 而應該使用遞歸
但是遞歸效率有問題, 不停的壓棧, 也很容易爆堆棧
所以對於某種遞歸, 尾遞歸, 編譯器會自動優化成循環執行, 避免多次使用堆棧
局限是, 不是什么情況都能寫成尾遞歸, 其實只有循環可以...
比clojuer好, 編譯器會自動進行優化
//while, 循環版本,oo def approximateLoop(initialGuess: Double): Double = { var guess = initialGuess while (!isGoodEnough(guess)) guess = improve(guess) guess } //遞歸版本,FP def approximate(guess: Double): Double = if (isGoodEnough(guess)) guess else approximate(improve(guess))
Pattern Matching
pattern matching (模式匹配是FP里面比較高級的特性), 可以參考一下clojure的Multimethods
那么Scala要如何來支持pattern matching?
Case Class
首先case class是用於讓你更加簡潔的使用pattern matching, 如果你想對一個class進行pattern matching, 最好在前面加上case, 如下面的例子(string和double的一元或二元操作)
abstract class Expr case class Var(name: String) extends Expr case class Number(num: Double) extends Expr case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
那么成為case class, 如何就能便於pattern matching?
1. 添加與類名一樣的工廠方法, 即創建類對象時不需要new, 更簡潔
scala> val v = Var("x") //替代new Var("x")
scala> val op = BinOp("+", Number(1), v)//new BinOp("+", new Number(1), v)
2. all arguments in the parameter list of a case class implicitly get a val prefix, so they are maintained as fields. 參數會默認隱含的加上val前綴, 所以可以當作field來用
scala> v.name res0: String = x scala> op.left res1: Expr = Number(1.0)
3. the compiler adds “natural” implementations of methods toString, hashCode, and equals to your class. 意味着可以用於println和"=="
scala> println(op) BinOp(+,Number(1.0),Var(x)) scala> op.right == Var("x") res3: Boolean = true
case class帶來的副作用就是你的類會比原來的大一些
Pattern matching
下面給個例子來說明, 實現方式非常類似switch…case
match關鍵字表明匹配前面的expr
每個case, case開頭, 跟着需要匹配的模式, =>后面加上執行語句
下面的例子的意思, 簡化expr, 當出現兩個-號, +0, 或*1的情況下, 就直接返回變量e就可以
def simplifyTop(expr: Expr): Expr = expr match { case UnOp("-", UnOp("-", e)) => e //UnOp構造器匹配(匹配類型和參數), "-"常量匹配(使用==), e變量匹配(任意值,並方便后面引用) case BinOp("+", e, Number(0)) => e case BinOp("*", e, Number(1)) => e // Multiplying by one case _ => expr //-通配符匹配, 匹配任意值,但無法引用 }
和switch case的區別
1. match是scala中的expression, 所以一定會返回值
2. 不用加break, 匹配即返回
3. 如果沒有匹配上, 會拋MatchError異常, 所以你必須考慮到所有case, 或加上默認case
Pattern種類
Wildcard patterns, 通配符模式
catch all, 用於忽略你不關心的部分
expr match { case BinOp(_, _, _) => println(expr +"is a binary operation") case _ => println("It's something else") }
Constant patterns, 常量模式
Any literal may be used as a constant. For example, 5, true, and "hello" are all constant patterns.
Also, any val or singleton object can be used as a constant.
def describe(x: Any) = x match { case 5 => "five" case true => "truth" case "hello" => "hi!" case Nil => "the empty list" //單例對象Nil匹配空列表 case _ => "something else" }
Variable patterns, 變量模式
變量模式, 和通配符一樣是catch all, 但是區別就在於, 后面可以通過變量引用
expr match { case 0 => "zero" case somethingElse => "not zero: "+ somethingElse }
變量模式和常量模式的區別
scala> import Math.{E, Pi} import Math.{E, Pi} scala> E match { case Pi => "strange math? Pi = "+ Pi //Pi是常量而非變量 case _ => "OK" } res10: java.lang.String = OK所有小寫字母開頭的為變量模式, 其他的都是常量模式
如果要用小寫字母開頭表示變量, 兩種方法, 加前綴this.pi, 加反引號`pi`
Constructor patterns, 構造器模式
這個模式使得scala的模式匹配顯得格外強大, 因為這樣可以deep match
比如下面的例子, 可以匹配BinOP類, 類參數+, e, Number類, Number類的參數0, 可以達到3層的deep match
expr match { case BinOp("+", e, Number(0)) => println("a deep match") case _ => }
Sequence patterns, 序列模式
expr match { case List(0, _, _) => println("found it") //第一個為0的length為3的List case _ => } expr match { case List(0, _*) => println("found it") //第一個為0,可變長的List case _ => }
Tuple patterns, 元組模式
特別之處在於, 可以用tuple給多個變量賦值
def tupleDemo(expr: Any) = expr match { case (a, b, c) => println("matched "+ a + b + c) case _ => }
Typed patterns, 類型模式
需要注意的是, 在類型模式下用s替換x
原因在於, x的類型是Any, 所以你無法調用x.length, 所以必須使用string s來替換x
def generalSize(x: Any) = x match { case s: String => s.length case m: Map[_, _] => m.size //匹配任意Map case _ => -1 }
可以看到這里很方便, 如果你對比一下使用傳統的isInstanceOf and asInstanceOf的方式
Type erasure, 類型擦除
可以看到運行時, 編譯器是無法check Map中數據的類型的, 因為Scala和Java一樣使用了erasure model of generics(泛型的擦除模式), 參數類型信息沒有保留到運行期
scala> def isIntIntMap(x: Any) = x match { case m: Map[Int, Int] => true //會報unchecked warning, 因為無法check Map中是否為Int case _ => false } scala> isIntIntMap(Map(1 >1)) res17: Boolean = true scala> isIntIntMap(Map("abc" >"abc")) res18: Boolean = true //可以看出, 其實是無法區分Map中數據類型的
但是, 數組是唯一的意外, 因為他們被特殊實現, 在運行期會保留參數類型信息,
scala> def isStringArray(x: Any) = x match { case a: Array[String] => "yes" case _ => "no" } scala> val ai = Array(1, 2, 3) scala> isStringArray(ai) res20: java.lang.String = no //對於數組可以識別出
Variable binding, 變量綁定
通過@將UnOp("abs", _)綁定到變量e上
expr match { case UnOp("abs", e @ UnOp("abs", _)) => e case _ => }
Pattern guards
其實就是在模式后面加上一定的條件判斷, 通過了才會執行后面的語句
比如下面的例子, 判斷x是否等於y,
scala> def simplifyAdd(e: Expr) = e match { case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2)) //加上條件判斷, 稱為pattern guard case _ => e }
Pattern overlaps
Scala支持多個Pattern重疊, 比如下面的例子, 匹配第一個的, 一定也會匹配第二個, 第三個
但順序不能反, 必須先具體的, 后general的, 否則編譯器會報錯
def simplifyAll(expr: Expr): Expr = expr match { case UnOp("-",UnOp("-",e)) => simplifyAll(e) // ‘-’is its own inverse case UnOp(op, e) => UnOp(op, simplifyAll(e)) case _ => expr }
Sealed classes
Sealed意味着只能在這個文件定義子類, 這樣就不能隨便在其他地方增加case類
這樣做的原因是, scala要求模式匹配時需要考慮到所有的情況, 所以程序員都需要知道到底有哪些case class
通過Sealed class, 不但程序員可以在該文件中找到所有的case class, 而且scala編譯器也會找到, 並在編譯時做檢查,
sealed abstract class Expr //sealed case class Var(name: String) extends Expr case class Number(num: Double) extends Expr case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
def describe(e: Expr): String = (e: @unchecked) match { //@unchecked告訴編譯器不用check這個match的case case Number(_) => "a number" case Var(_) => "a variable" }
5 Actors and Concurrency
在處理並發上, Scala采取了和Clojure完全不同的思路, 采取actor模式盡量避免並發沖突的可能性, 用於補充Java傳統的並發模式
Actors and message passing
Actor模式就不解釋了, 這里actor就是一個線程
下面定義的echoActor, 會不停的讀inbox, 並打印出收到的message
val echoActor = actor { while (true) { receive { case msg => println("received message: "+ msg) } } }
然后, 如何給actor發消息? 很簡單, 如下
scala> echoActor ! "hi there" received message: hi there //echoActor線程收到message, 並打印出來
並且這里receive只會接收匹配的message(即匹配receive中的case的), 對於不匹配的message不會去處理
將普通線程當作actor
在scala中也可以將native線程當作actor來用, 並且通過Actor.self來當前線程當作actor看
import scala.actors.Actor._ scala> self ! "hello" //給當前線程actor發送消息 scala> self.receive { case x => x } //當前線程receive res6: Any = hello
這里需要先發message, 再recieve, 因為receive是block函數, 沒有message不會返回
當然也可以使用receiveWithin, 來設置timeout
scala> self.receiveWithin(1000) { case x => x } // wait a sec! res7: Any = TIMEOUT
高效的react方法
actor模式問題是, 每個actor都需要一個線程, 所以如果需要定義大量的actor效率會比較低
JVM只能創建幾千個線程, 並且線程間的切換也是比較低效的
所以使用react方法可以共享線程以提高效率...
原理在於, Scala actor模塊會維護一個線程池, 用於分配線程給每個線程
對於receive方法, 分配的線程會一直被actor占住, 因為receive會返回值需要保持當前的調用棧
而react方法, 不會返回, 不需要保持調用棧, 所以執行完這個線程就可以被釋放, 用於其他的actor
object NameResolver extends Actor { import java.net.{InetAddress, UnknownHostException} def act() { react { case (name: String, actor: Actor) => actor ! getIp(name) act() case "EXIT" => println("Name resolver exiting.")// quit case msg => println("Unhandled message: "+ msg) act() } } def getIp(name: String): Option[InetAddress] = { try { Some(InetAddress.getByName(name)) } catch { case _:UnknownHostException => None } } }
How react works
A return type of Nothing indicates a function will never return normally, but instead will always complete abruptly with an exception. And indeed, this is true of react.
react的返回值是Nothing, 意思是不會正常返回, 即以異常作為react的結束
The actual implementation of react is not as simple as the following description, and subject to change, but conceptually you can think of react as working like this:
When you call start on an actor, the start method will in some way arrange things such that some thread will eventually call act on that actor. If that act method invokes react, the react method will look in the actor’s mailbox for a message the passed partial function can handle. (It does this the same way as receive, by passing candidate messages to the partial function’s isDefinedAt method.)
前面都是和receive一樣的, 線程調用act, act調用react從actor的mailbox里面check是否有需要處理的message
If it finds a message that can be handled, react will schedule the handling of that message for later execution and throw an exception.
If it doesn’t find one, it will place the actor in “cold storage,” to be resurrected if and when it gets more messages in its mailbox, and throw an exception.
In either case, react will complete abruptly with this exception, and so will act. The thread that invoked act will catch the exception, forget about the actor, and move on to other duties.
如果找到需要處理的message, react就會處理這條message, 最終拋出exception, 如果找不到合適的message, 會將這個actor暫時休眠, 並拋出exception
可以看到, 這里的關鍵在於, react一定會以exception結束, 最終調用act的線程會catch到這個exception, 知道react已經完成, 於是接着處理其他的事情, 所以達到線程重用
This is why if you want react to handle more than the first message, you’ll need to call act again from inside your partial function, or use some other means to get react invoked again.
使用Actor.Loop來反復執行消息處理, 而不能用while
def act() { loop { react { case (name: String, actor: Actor) => actor ! getIp(name) case msg => println("Unhandled message: " + msg) } } }
Actors should not block
主Actor不應被阻塞, 因為這樣他就不能及時的響應其他消息
處理的方法, 就是阻塞邏輯放到一個新的actor里面執行, 因為這個actor不會接收其他消息, 所以被阻塞是沒有影響的
val sillyActor2 = actor { def emoteLater() { val mainActor = self actor { //使用新的actor Thread.sleep(1000) //阻塞邏輯 mainActor ! "Emote" //將結果返回給主actor } } var emoted = 0 emoteLater() loop { react { case "Emote" => println("I'm acting!") emoted += 1 if (emoted < 5) emoteLater() //這里沒有直接執行阻塞邏輯, 而是調用新的actor來執行 case msg => println("Received: "+ msg) } } }
實際運行結果, 可見發送給sillyActor2的消息立即會得到處理, 不會被阻塞
scala> sillyActor2 ! "hi there"
scala> Received: hi there
I'm acting!
I'm acting!
I'm acting!
Make messages self-contained
通過case class來定義消息格式
否則如果只是簡單的, lookerUpper ! ("www.scalalang.org", self), 其他人很難理解你的消息
import scala.actors.Actor._ import java.net.{InetAddress, UnknownHostException} case class LookupIP(name: String, respondTo: Actor) //通過case class來定義消息格式, 來便於理解 case class LookupResult( name: String, address: Option[InetAddress] ) object NameResolver2 extends Actor { def act() { loop { react { case LookupIP(name, actor) => actor ! LookupResult(name, getIp(name)) } } } def getIp(name: String): Option[InetAddress] = { // As before (in Listing 30.3) } }