Scala函數式編程(六) 懶加載與Stream

前情提要html

Scala函數式編程指南(一) 函數式思想介紹java

scala函數式編程(二) scala基礎語法介紹編程

Scala函數式編程(三) scala集合和函數api

Scala函數式編程(四)函數式的數據結構 上緩存

Scala函數式編程(四)函數式的數據結構 下性能優化

Scala函數式編程(五) 函數式的錯誤處理數據結構

何時效率複習最高,毫無疑問是考試前的最後一晚上,一樣的道理還有寒暑假最後一天作做業最高效。學界有一個定理:deadline是第一輩子產力,說的就是這個事情。多線程

一樣的,這個道理徹底能夠推廣到函數式編程中來,而懶加載(scala的lazy關鍵字)就是這樣的東西。app

在函數式編程中,由於要維持不變性,故而須要更多的存儲空間,這一點在函數式數據結構中有說到。懶加載能夠說會在必定程度上解決這個問題,同時經過緩存數據還能提升一些運行效率,以及經過面向表達式編程提升系統的模塊化。ide

這一節先介紹lazy的具體內容,及其好處,而後經過Stream這一數據結構討論懶加載更多應用場景以及懶加載是如何實現性能優化的。

1.scala懶加載lazy

1.1 什麼是懶加載

懶加載,顧名思義就是一個字懶。就像老闆讓你去幹活,剛叫的時候你不會去幹,只有等到着急的時候,催你的時候你纔會去幹。懶加載就是這樣的東西。

咱們直接用命令行測試下:

//右邊是一個表達式,這裏不是懶加載,直接求值
scala> val x = { println("x"); 15 }
x
x: Int = 15

//使用了懶加載,這裏和上面的右側是相似的,不過不會當即求值
scala> lazy val y = { println("y"); 13 }
y: Int = <lazy>

//x的值變成15,也就是表達式的結果
scala> x
res2: Int = 15

//懶加載在真正調用的時候,才運行表達式的內容,打印y,並返回值
scala> y
y
res3: Int = 13

//lazy已經緩存的表達式的內容,因此不會再運行表達式裏面的東西,也就是表達式內容只運行一次
scala> y
res4: Int = 13

看上面代碼就明白了,懶加載就是讓表達式裏面的計算延遲,而且只計算一次,而後就會緩存結果。

值得一提的是,懶加載只對表達式和函數生效,若是直接定義變量,那是沒什麼用的。由於懶加載就是讓延遲計算,你直接定義變量那計算啥啊。。。

說完lazy這個東西,那就來講說它究竟有什麼用。

1.2 懶加載的好處

初次看到這個東西,會疑惑,懶加載有什麼用?其實它的用處可不小。

lazy的一個做用,是將推遲複雜的計算,直到須要計算的時候才計算,而若是不使用,則徹底不會進行計算。這無疑會提升效率。

而在大量數據的狀況下,若是一個計算過程相互依賴,就算後面的計算依賴前面的結果,那麼懶加載也能夠和緩存計算結合起來,進一步提升計算效率。嗯,有點相似於spark中緩存計算的思想。

除了延遲計算,懶加載也能夠用於構建相互依賴或循環的數據結構。我這邊再舉個從stackOverFlow看到的例子:

這種狀況會出現棧溢出,由於無限遞歸,最終會致使堆棧溢出。

trait Foo { val foo: Foo }
case class Fee extends Foo { val foo = Faa() }
case class Faa extends Foo { val foo = Fee() }

println(Fee().foo)
//StackOverflowException

而使用了lazy關鍵字就不會了,由於通過lazy關鍵字修飾,變量裏面的內容壓根就不會去調用。

trait Foo { val foo: Foo }
case class Fee extends Foo { lazy val foo = Faa() }
case class Faa extends Foo { lazy val foo = Fee() }

println(Fee().foo)
//Faa()

固然上面這種方法也可讓它所有求值,在後面stream的時候再介紹。

1.3 其餘語言的懶加載

看起來懶加載是很神奇的東西,但其實這個玩意也不是什麼新鮮東西。一說你可能就會意識到了,其實懶加載就是單例模式中的懶漢構造法。

如下是scala中的懶加載:

class LazyTest {
  //懶加載定義一個變量
  lazy val msg = "Lazy"
}

若是轉成一樣功能的java代碼:

class LazyTest {
  public int bitmap$0;
  private String msg;

  public String msg() {
    if ((bitmap$0 & 1) == 0) {
        synchronized (this) {
            if ((bitmap$0 & 1) == 0) {
                synchronized (this) {
                    msg = "Lazy";
                }
            }
            bitmap$0 = bitmap$0 | 1;
        }
    }
    return msg;
  }

}

其實說白了,就是考慮多線程狀況下,運用懶漢模式建立一個單例的代碼。只不過在scala中,提供了語法級別的支持,因此懶加載使用起來更加方便。

OK,介紹完懶加載,咱們再說說一個息息相關的數據結構,Stream(流)。

2.Stream數據結構

Stream數據結構,根據名字判斷,就知道這是一個流。直觀得說,Stream能夠看做一個特殊點的List,特殊在於Stream自然就是「懶」的(java8也新增了叫Stream的數據結構,但和scala的仍是有點區別的,這一點要區分好)。

直接看代碼吧:

//新建List
scala> val li = List(1,2,3,4,5)
li: List[Int] = List(1, 2, 3, 4, 5)

//新建Stream
scala> val stream = Stream(1,2,3,4,5)
stream: scala.collection.immutable.Stream[Int] = Stream(1, ?)

//每一個Stream有兩個元素,一個head表示當前元素,tail表示除當前元素後面的其餘元素,也可能爲空
//就跟鏈表同樣
scala> stream.head
res21: Int = 1

//後一個元素,相似鏈表
scala> stream.tail
res20: scala.collection.immutable.Stream[Int] = Stream(2, ?)

List能夠直接轉成Stream,也能夠新生成,一個Stream和鏈表是相似的,有一個當前元素,和一個指向下一個元素的句柄。

可是!Stream不會計算,或者說獲取下一個元素的狀態和內容。也就是說,在真正調用前,當前是Stream是不知道它指向下一個元素到底是什麼,是否是空的?

那麼問題來了,爲嘛要大費周章搞這麼個Stream?

其實Stream能夠作不少事情,這裏簡單介紹一下。首先說明,不管是懶加載仍是Stream,使用它們很大程度是爲了提升運行效率或節省空間。

獲取數據

Stream特別適合在不肯定量級的數據中,獲取知足條件的數據。這裏給出一個大佬的例子:
Scala中Stream的應用場景及事實上現原理

這個例子講的是在50個隨機數中,獲取前3個能被整除的數字。固然直接寫個while很簡單,但若是要用函數式的方式就不容易了。

而若是要沒有一絲一毫的空間浪費,那就只有使用Stream了。

再舉個例子,若是要讀取一個很是大的文件,要讀取第一個'a'字符前面的全部數據。

若是使用getLine或其餘iterator的api,那要用循環或遞歸迭代去獲取,而若是用Stream,只需一行代碼。

Source.fromFile("path").toStream.takeWhile(_ != 'a')

道理和隨機數的那個例子是同樣的。

消除中間結果

這是《scala函數式編程》書裏面的例子,這裏拿來講一說。

有這樣一行代碼:

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)

若是讓它執行,那麼會先執行map方法,生成一箇中間結果,再執行filter,返回一箇中間結果,再執行map獲得最終結果,流程大概以下:

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3) => 
//生成中間結果
List(11,12,13,14).filter(_ % 2 == 0).map(_ * 3) => //又生成中間結果
List(12,14).map(_ * 3) =>
//獲得最終結果
List(36,42)

看,上面例子中,會生成多箇中間的List,但其實這些是不必的,咱們徹底能重寫一個While,直接在一個代碼塊中實現map(_ + 10).filter(_ % 2 == 0).map(_ * 3)這三個函數的功能,但卻不夠優雅。而Stream可以無縫作到這點。

能夠在idea中用代碼調試功能追蹤一下,由於Stream天生懶的緣由,它會讓一個元素直接執行所有函數,第一個元素產生結果後,再執行下一個元素,避免中間臨時數據產生。看流程:

Stream(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).toList =>
//對第一個元素應用map
Stream(11,Stream(2,3,4)).map(_ + 10).filter(_ % 2 == 0).toList =>
//對第一個元素應用filter
Stream(2,3,4).map(_ + 10).filter(_ % 2 == 0).toList  =>
//對第二個元素應用map
Stream(12,Stream(3,4)).map(_ + 10).filter(_ % 2 == 0).toList
//對第二個元素應用filter生成結果
12 :: Stream(3,4).map(_ + 10).filter(_ % 2 == 0).toList  =>

......以此類推

經過Stream數據結構,能夠優雅得去掉臨時數據所產生的負面影響。

小結

總而言之,懶加載主要是爲了可以在必定程度上提高函數式編程的效率,不管是空間效率仍是時間效率。這一點看Stream的各個例子就明白了,Stream這種數據結構自然就是懶的。

同時懶加載更重要的一點是經過分離表達式和值,提高了模塊化。這句話聽起來比較抽象,仍是得看回1.2 懶加載的好處這一節的例子。所謂值和表達式分離,在這個例子中,就是當調用Fee().foo的時候,不會馬上要求得它的值,而只是得到了一個表達式,表達式的值暫時並不關心。這樣就將表達式和值分離開來,而且模塊化特性更加明顯!從這個角度來看,這一點和Scala函數式編程(五) 函數式的錯誤處理介紹的Try()錯誤處理有些相似,都是關注表達式而不關注具體的值,其核心歸根結底就是爲了提高模塊化

以上~

相關文章
相關標籤/搜索