scala 從入門到入門+

新手向,面向剛從java過渡到scala的同窗,目的是寫出已已易於維護和閱讀的代碼.

寫了一份更全面,帶習題的文檔在 https://fordeal-smalldata.git...
時間充裕的朋友能夠試試

從語句到表達式

語句(statement): 一段可執行的代碼
表達式(expression): 一段能夠被求值的代碼

在Java中語句和表達式是有區分的,表達式必須在return或者等號右側,而在scala中,一切都是表達式.php

一個例子:
假設咱們在公司的內網和外網要從不一樣的域名訪問同樣的機器html

//Java代碼
String urlString = null;
String hostName = InetAddress.getLocalHost().getHostName();
if (isInnerHost(hostName)) {
  urlString = "http://inner.host";
} else {
  urlString = "http://outter.host";
}

剛轉到scala的人極可能這麼寫java

var urlString: String = null
var hostName = InetAddress.getLocalHost.getHostName
if (isInnerHost(hostName)) {
  urlString = "http://inner.host"
} else {
  urlString = "http://outter.host"
}

咱們讓它更像scala一點吧mysql

val hostName = InetAddress.getLocalHost.getHostName
val urlString = if (isInnerHost(hostName)) {
  "http://inner.host"
} else {
  "http://outter.host"
}
這樣作的好處都有啥?
  1. 代碼簡練,符合直覺
  2. urlString 是值而不是變量,有效防止 urlString 在後續的代碼中被更改(編譯時排錯)

不少時候,咱們編程時說的安全並非指怕被黑客破壞掉,而是預防本身由於逗比而讓程序崩了.react

純函數和非純函數

純函數(Pure Function)是這樣一種函數——輸入輸出數據流全是顯式(Explicit)的。
顯式(Explicit)的意思是,函數與外界交換數據只有一個惟一渠道——參數和返回值;函數從函數外部接受的全部輸入信息都經過參數傳遞到該函數內部;函數輸出到函數外部的全部信息都經過返回值傳遞到該函數外部。git

若是一個函數經過隱式(Implicit)方式,從外界獲取數據,或者向外部輸出數據,那麼,該函數就不是純函數,叫做非純函數(Impure Function)。
隱式(Implicit)的意思是,函數經過參數和返回值之外的渠道,和外界進行數據交換。好比,讀取全局變量,修改全局變量,都叫做以隱式的方式和外界進行數據交換;好比,利用I/O API(輸入輸出系統函數庫)讀取配置文件,或者輸出到文件,打印到屏幕,都叫作隱式的方式和外界進行數據交換。程序員

//一些例子
//純函數
def add(a:Int,b:Int) = a + b
//非純函數
var a = 1
def addA(b:Int) = a + b
 
def add(a:Int,b:Int) = {
  println(s"a:$a b:$b")
  a + b
}
def randInt() = Random.nextInt()

純函數的好處(來自維基百科)

  • 無狀態,線程安全,不須要線程同步.
  • 純函數相互調用組裝起來的函數,仍是純函數.
  • 應用程序或者運行環境(Runtime)能夠對純函數的運算結果進行緩存,運算加快速度.

純函數的好處(來自個人經驗)

  • 單元測試很是方便!
  • 分佈式/併發環境下,斷點調試的方式無覺得繼,你須要單元測試.

單元測試什麼的,趕忙去 http://www.scalatest.org 試試吧es6

惰性求值/Call by name

維基百科中惰性求值的解釋
惰性求值(Lazy Evaluation),又稱惰性計算、懶惰求值,是一個計算機編程中的一個概念,它的目的是要最小化計算機要作的工做。它有兩個相關而又有區別的含意,能夠表示爲「延遲求值」和「最小化求值」,本條目專一前者,後者請參見最小化計算條目。除能夠獲得性能的提高外,惰性計算的最重要的好處是它能夠構造一個無限的數據類型。
惰性求值的相反是及早求值,這是一個大多數編程語言所擁有的普通計算方式。

惰性求值不是新鮮事

import scala.io.Source.fromFile
val iter: Iterator[String] =
  fromFile("sampleFile")
    .getLines()

文件迭代器就用到了惰性求值.
用戶能夠徹底像操做內存中的數據同樣操做文件,然而文件只有一小部分傳入了內存中.github

用lazy關鍵詞指定惰性求值

lazy val firstLazy = {
  println("first lazy")
  1
}
lazy val secondLazy = {
  println("second lazy")
  2
} 
def add(a:Int,b:Int) = {
  a+b
}
//在 scala repl 中的結果
scala> add(secondLazy,firstLazy)
second lazy
first lazy
res0: Int = 3

res0: Int = 3

second lazy 先於 first lazy輸出了sql

Call by value 就是函數參數的惰性求值

def firstLazy = {
  println("first lazy")
  1
}
def secondLazy = {
  println("second lazy")
  2
}
def chooseOne(first: Boolean, a: Int, b: Int) = {
  if (first) a else b
}
def chooseOneLazy(first: Boolean, a: => Int, b: => Int) = {
  if (first) a else b
}
chooseOne(first = true, secondLazy, firstLazy)
//second lazy
//first lazy
//res0: Int = 2
chooseOneLazy(first = true, secondLazy, firstLazy)
//second lazy
//res1: Int = 2

對於非純函數,惰性求值會產生和當即求值產生不同的結果.

一個例子,假設你要創建一個本地緩存

//須要查詢mysql等,可能來自於一個第三方jar包
def itemIdToShopId: Int => Int  
var cache = Map.empty[Int, Int]
def cachedItemIdToShopId(itemId: Int):Int = {
  cache.get(itemId) match {
    case Some(shopId) => shopId
    case None =>
      val shopId = itemIdToShopId(itemId)
      cache += itemId -> shopId
      shopId
  }
}
  • 羅輯沒什麼問題,但測試的時候不方便連mysql怎麼辦?
  • 若是第三方jar包發生了改變,cachedItemIdToShopId也要發生改變.
//用你的本地mock來測試程序
def mockItemIdToSHopId: Int => Int
def cachedItemIdToShopId(itemId: Int): Int ={  
  cache.get(itemId) match {    
    case Some(shopId) => shopId
   case None =>    
      val shopId = mockItemIdToSHopId(itemId)
      cache += itemId -> shopId
     shopId    
  }    
}
  • 在測試的時候用mock,提交前要換成線上的,反覆測試的話要反覆改動,很是使人沮喪.
  • 手工操做容易忙中出錯.
//將遠程請求的結果做爲函數的一個參數
def cachedItemIdToShopId(itemId: Int, remoteShopId: Int): Int = {    
  cache.get(itemId) match {    
    case Some(shopId) => shopId    
    case None =>    
     val shopId = remoteShopId    
     cache += itemId -> shopId    
      shopId
  }    
}
//調用這個函數
cachedItemIdToShopId(itemId,itemIdToShopId(itemId))
  • 函數對mysql的依賴沒有了
  • 不須要在測試和提交時切換代碼
  • 貌似引入了新問題?

沒錯,cache根本沒有起應有的做用,函數每次執行的時候都調用了itemIdToShopId從遠程取數據

//改爲call by name就沒有這個問題啦
def cachedItemIdToShopId(itemId: Int, remoteShopId: =>Int): Int = {    
  cache.get(itemId) match {    
    case Some(shopId) => shopId    
    case None =>    
     val shopId = remoteShopId    
     cache += itemId -> shopId    
      shopId
  }    
}
//調用這個函數
cachedItemIdToShopId(itemId,itemIdToShopId(itemId))
  • 函數對mysql的依賴沒有了
  • 不須要在測試和提交時切換代碼
  • 只在須要的時候查詢遠程庫

Tuple/case class/模式匹配

Tuple爲編程提供許多便利

  • 函數能夠經過tuple返回多個值
  • tuple能夠存儲在容器類中,代替java bean
  • 能夠一次爲多個變量賦值

使用tuple的例子

val (one, two) = (1, 2)        
one //res0: Int = 1    
two //res1: Int = 2            
def sellerAndItemId(orderId: Int): (Int, Int) =
   orderId match {    
    case 0 => (1, 2)    
 }            
val (sellerId, itemId) = sellerAndItemId(0)
sellerId // sellerId: Int = 1
itemId // itemId: Int = 2        
val sellerItem = sellerAndItemId(0)
sellerItem._1 //res4: Int = 1
sellerItem._2 //res5: Int = 2

用模式匹配增長tuple可讀性

val sampleList = List((1, 2, 3), (4, 5, 6), (7, 8, 9))
sampleList.map(x => s"${x._1}_${x._2}_${x._3}")
//res0: List[String] = List(1_2_3, 4_5_6, 7_8_9)
sampleList.map {    
  case (orderId, shopId, itemId) =>
    s"${orderId}_${shopId}_$itemId"
}    
//res1: List[String] = List(1_2_3, 4_5_6, 7_8_9)

上下兩個map作了一樣的事情,但下一個map爲tuple中的三個值都給了名字,增長了代碼的可讀性.

match和java和switch很像,但有區別

  1. match是表達式,會返回值
  2. match不須要」break」
  3. 若是沒有任何符合要求的case,match會拋異常,由於是表達式
  4. match能夠匹配任何東西,switch只能匹配數字或字符串常量
//case若是是常量,就在值相等時匹配.
//若是是變量,就匹配任何值.
def describe(x: Any) = x match {
   case 5 => "five"    
   case true => "truth"    
   case "hello" => "hi!"    
   case Nil => "the empty list"
   case somethingElse => "something else " + somethingElse    
}

case class,tuple以及列表均可以在匹配的同時捕獲內部的內容.

case class Sample(a:String,b:String,c:String,d:String,e:String)
def showContent(x: Any) =
 x match {        
  case Sample(a,b,c,d,e) =>    
  s"Sample $a.$b.$c.$d.$e"    
  case (a,b,c,d,e) =>    
  s"tuple $a,$b,$c,$d,$e"    
  case head::second::rest =>    
  s"list head:$head second:$second rest:$rest"
}

Case class

  1. 模式匹配過程當中其實調用了類的unapply方法
  2. Case class 是爲模式匹配(以及其餘一些方面)提供了特別的便利的類
  3. Case class 仍是普通的class,可是它自動爲你實現了apply,unapply,toString等方法
  4. 其實tuple就是泛型的case class

用 option 代替 null

null 的問題

Map<String, String> map = ???
String valFor2014 = map.get(「1024」); // null

if (valFor1024 == null)
    abadon();
else doSomething();
  • null到底表明key找不到仍是說1024對應的值就是null?
  • 某年某月某日,我把爲null則abandon這段代碼寫了100遍.

option介紹

  • option能夠看做是一個容器,容器的size是1或0
  • Size爲1的時候就是一個Some[A](x: A),size爲0的時候就是一個None

看看scala的map

def get(key: A): Option[B]

def getOrElse[B1 >: B](key: A, default: => B1): B1 = get(key) match {
  case Some(v) => v
  case None => default
}
  • 能夠區分Map中到底又沒有這個key.
  • 我見過許多java項目本身實現了getOrElse這個方法並放在一個叫作MapUtils的類裏.
  • 爲何java通過這麼多代演進,Map仍然沒有默認包含這個方法,一直想不通.

(寫完這段忽然發現java8開始包含getOrDefault了)

好像沒有太大區別?

確實可以區分Map是無值仍是值爲null了.
可是if(爲null) 則 abandon 要寫一百遍.
case Some(v) => v
case None => default
彷佛也得寫一百遍.

不,不是這樣的
不要忘了option是個容器
http://www.scala-lang.org/api...

試試容器裏的各類方法

val a: Option[String] = Some("1024")
val b: Option[String] = None
a.map(_.toInt)
//res0: Option[Int] = Some(1024)
b.map(_.toInt)
//res1: Option[Int] = None,不會甩exception
a.filter(_ == "2048")
//res2: Option[String] = None
b.filter(_ == "2048")
//res3: Option[String] = None
a.getOrElse("2048")
//res4: String = 1024
b.getOrElse("2048")
//res5: String = 2048
a.map(_.toInt)
  .map(_ + 1)
  .map(_ / 5)
  .map(_ / 2 == 0) //res6: Option[Boolean] = Some(false)
//若是是null,恐怕要一連check abandon四遍了

option配合其餘容器使用

val a: Seq[String] =
  Seq("1", "2", "3", null, "4")
val b: Seq[Option[String]] =
  Seq(Some("1"), Some("2"), Some("3"), None, Some("4"))

a.filter(_ != null).map(_.toInt)
//res0: Seq[Int] = List(1, 2, 3, 4)
//若是你忘了檢查,編譯器是看不出來的,只能在跑崩的時候拋異常
b.flatMap(_.map(_.toInt))
//res1: Seq[Int] = List(1, 2, 3, 4)
  • option幫助你把錯誤扼殺在編譯階段
  • flatMap則能夠在過濾空值的同時將option恢復爲原始數據.

scala原生容器類都對option有良好支持

Seq(1,2,3).headOption
//res0: Option[Int] = Some(1)

Seq(1,2,3).find(_ == 5)
//res1: Option[Int] = None

Seq(1,2,3).lastOption
//res2: Option[Int] = Some(3)

Vector(1,2,3).reduceLeft(_ + _)
//res3: Int = 6

Vector(1,2,3).reduceLeftOption(_ + _)
//res4: Option[Int] = Some(6)
//在vector爲空的時候也能用

Seq("a", "b", "c", null, "d").map(Option(_))
//res0: Seq[Option[String]] =
// List(Some(a), Some(b), Some(c), None, Some(d))
//原始數據轉換成option也很方便

用Try類保存異常

傳統異常處理的侷限性

try {
  1024 / 0
} catch {
  case e: Throwable => e.printStackTrace()
}

用try-catch的模式,異常必須在拋出的時候立刻處理.
然而在分佈式計算中,咱們極可能但願將異常集中到一塊兒處理,來避免須要到每臺機器上單獨看錯誤日誌的窘態.

val seq = Seq(0, 1, 2, 3, 4)
 //seq: Seq[Int] = List(0, 1, 2, 3, 4)

val seqTry = seq.map(x => Try {
  20 / x
})
//seqTry: Seq[scala.util.Try[Int]] = List(Failure(java.lang.ArithmeticException: devide by zero),Success(20), Success(10), Success(6), Success(5))

val succSeq = seqTry.flatMap(_.toOption)
//succSeq: Seq[Int] = List(20, 10, 6, 5) Try能夠轉換成Option
val succSeq2 = seqTry.collect {
  case Success(x) => x
}
//succSeq2: Seq[Int] = List(20, 10, 6, 5) 和上一個是同樣的
val failSeq: Seq[Throwable] = seqTry.collect {
  case Failure(e) => e
}
//failSeq: Seq[Throwable] = List(java.lang.ArithmeticException: devide by zero)

Try實例能夠序列化,而且在機器間傳送.

函數是一等公民

一個需求

  • 假設咱們須要檢查許多的數字是否符合某一範圍
  • 範圍存儲在外部系統中,而且可能隨時更改
  • 數字範圍像這樣存儲着」>= 3,< 7」

一個java版本

List<String> params = new LinkedList<>();
List<Integer> nums = new LinkedList<>();
List<String> marks = new LinkedList<>();

public JavaRangeMatcher(List<String> params) {
    this.params = params;
    for (String param : params) {
        String[] markNum = param.split(" ");
        marks.add(markNum[0]);
        nums.add(Integer.parseInt(markNum[1]));
    }
}

public boolean check(int input) {
    for (int i = 0; i < marks.size(); i++) {
        int num = nums.get(i);
        String mark = marks.get(i);
        if (mark.equals(">") && input <= num) return false;
        if (mark.equals(">=") && input < num) return false;
        if (mark.equals("<") && input >= num) return false;
        if (mark.equals("<=") && input > num) return false;
    }
    return true;
}

List<String> paramsList = new LinkedList<String>() {{
    add(「>= 3」);
    add(「< 7」);
}};
JavaRangeMatcher matcher = new JavaRangeMatcher(paramsList);
int[] inputs = new int[]{1, 3, 5, 7, 9};
for (int input : inputs) {
    System.out.println(matcher.check(input));
}
//給本身有限的時間,想一想又沒有性能優化的餘地
//咱們一塊兒來跑跑看

一個 scala 版本

def exprToInt(expr: String): Int => Boolean = {
  val Array(mark, num, _*) = expr.split(" ")
  val numInt = num.toInt
  mark match {
    case "<" => numInt.>
    case ">" => numInt.<
    case ">=" => numInt.<=
    case "<=" => numInt.>=
  } //返回函數的函數
}

case class RangeMatcher(range: Seq[String]) {
  val rangeFunc: Seq[(Int) => Boolean] = range.map(exprToInt)

  def check(input: Int) = rangeFunc.forall(_(input))
}

def main(args: Array[String]) {
  val requirements = Seq(">= 3", "< 7")
  val rangeMatcher = RangeMatcher(requirements)
  val results = Seq(1, 3, 5, 7, 9).map(rangeMatcher.check)
  println(results.mkString(","))
  //false,true,true,false,false
}

關於性能

這裏有一個性能測試網站

我對於網站測試的結果,我總結的狀況就是兩點.

  1. 排在後面的基本都是動態類型語言,靜態類型語言相對容易優化到性能差很少的結果.
  2. 同一個語言代碼寫得好差產生的性能差別,遠遠比各類語言最好的代碼性能差別大.

總的來講,程序員越自由,程序性能就越差

不過也有反例,咱們以前那個程序就是.

//java版本
public static void main(String[] args) {
    List<String> paramsList = new LinkedList<String>() {{
        add(">= 3");
        add("< 7");
    }};
    JavaRangeMatcher matcher = new JavaRangeMatcher(paramsList);
    Random random = new Random();
    long timeBegin = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        int input = random.nextInt() % 10;
        matcher.check(input);
    }
    long timeEnd = System.currentTimeMillis();
    System.out.println("java 消耗時間: " + (timeEnd - timeBegin) + " 毫秒");
    //java 消耗時間: 3263 毫秒
}
//scala版本
def main(args: Array[String]) {
  val requirements = Seq(">= 3", "< 7")
  val rangeMatcher = RangeMatcher(requirements)
  val timeBegin = System.currentTimeMillis()
  0 until 100000000 foreach {
    case _ =>
      rangeMatcher.check(Random.nextInt(10))
  }
  val timeEnd = System.currentTimeMillis()
  println(s"scala 消耗時間 ${timeEnd - timeBegin} 毫秒")
  //scala 消耗時間 2617 毫秒
}

想一想這是爲何?

推薦資源

  • 盡情地使用worksheet吧!
  • 盡情地用IDE查看標準庫的源代碼吧!
  • 推薦coursera上的課程progfunreactive
  • 盡情地查看文檔,推薦軟件Dash
相關文章
相關標籤/搜索