2019-04-14
關鍵字: Scala、Scala控制抽象、Scala高階函數
本篇文章系筆者根據當前掌握的知識對 Scala 控制抽象的教材知識總結,不保證文章所述內容的絕對、完全正確性。
在 《快學Scala》 一書中,控制抽象被描述為是一系列語句的聚集,是一種特殊的函數,因為它是本質上只是對一系列語句的封裝,所以它理應:
1. 沒有參數輸入;
2. 沒有值返回。
教材中還給出了兩段代碼示例來解釋控制抽象。但這兩段代碼對於 Scala 初學者來說,可能沒那么好理解。這篇文章主要就針對教材中的示例作個白話解釋。
第 1 個例子
1 def runInThread(block: () => Unit) { 2 new Thread { 3 override def run() { 4 block() 5 } 6 } 7 }
這個例子的功能倒好理解,它就是一個將傳入的代碼塊放到一個全新的子線程里去執行的功能。這種場景在日常工作過程中是一很常見的場景,但是筆者認為這個例子對於初學者理解 控制抽象 卻不是一個合適的例子。因為,有很多初學者甚至還沒來得學習 Scala 中的對象實例化及匿名內部類的知識,雖然在 Scala 中它長的與 Java 非常像,但對於猜測的知識而言,心里始終會有個芥蒂。本來是只想學習控制抽象知識點的,突然穿插一個可能是匿名內部類,可能是線程的知識點進來,會加重讀者學習負擔,甚至打擊學習信心。因此我准備換一個示例代碼,這個示例代碼除了控制抽象的知識以外,其余的全是單純邏輯代碼,不會對讀者對控制抽象的學習造成額外的負擔。
新的示例代碼就定義一個類似 “購物” 的功能模塊。封裝一個 myShop 函數,這個函數開放給顧客調用,每次調用都表示一位顧客的購買行為。
1 def myShop(block: () => Unit) { 2 3 println("Welcome in!") 4 block() 5 println("Thanks for coming!") 6 7 }
好,接下來就開始我們的學習。
首先,上面我們有提到:控制抽象是一系列語句的聚集。因此,在示例代碼中,千萬不要覺得 def myShop 就是控制抽象。block 作為 “語句塊” 才應該是控制抽象!

block 才是控制抽象
函數 myShop 的參數的變量名稱是 block ,它的名稱也已經很直白了,說明這里就應該傳 “一塊” 代碼進來。而它的參數則是一個匿名函數類型 () => Unit ,它沒有輸入參數,也沒有值返回。隨后便是函數體的聲明,這種函數名后面直接接大括號來寫函數體的方式在 Scala 中被稱為 “過程” ,簡單說就是聲明這個函數( myShop 函數 )是一個沒有返回值的函數。在給這個 myShop 函數傳參時,直接將你需要的語句塊一骨腦扔進去就好。
《快學Scala》 中調用 runInThread 函數的方式是
1 runInThread { 2 () => 3 println("Hi") 4 Thread.sleep(10000) 5 println("Bye") 6 }
類似地,在我們這個 myShop 函數中,也可以有如下調用方式
1 myShop { 2 () => 3 println("I want a pencil") 4 println("I want a book") 5 println("I want your wechat") 6 }
myShop 例子的執行結果如下

emm... 這種調用方式乍看上去感覺像是在寫一個函數體一樣,感覺怪怪的。但其實只要你了解了 Scala 中 圓括號 與 大括號 的作用以后,這段代碼你看起來就沒什么毛病的了。
在 Scala 中,調用函數時,一般既可以使用圓括號來傳參,也可以使用大括號來傳參。不信?

scala 傳參的方式
所以,上面不管是 runInThread 還是 myShop 的函數調用,都只是在傳參而已。圓括號與大括號的區別在於可以傳遞的代碼量而已。圓括號只能傳遞一條語句,而大括號可以傳遞多條語句。因此,對於我們的 myShop 的例子來說,假如某個人只想買一件商品,那么,完全可以使用以下的調用方式。同時,由於控制抽象函數不需要傳參,所以一個 空的參數列表括號 在這也挺多余的,所以,函數聲明和調用都可以簡化一下。
1 def myShop(block: => Unit) { 2 3 println("Welcome in!") 4 block 5 println("Thanks for coming!") 6 7 } 8 9 def main(args: Array[String]){ 10 myShop( println("I wanna buy a condom") ) 11 }
怎么樣,這樣看起來是不是順眼多了? 它的執行結果當然也是沒毛病的。

當然,即使是只想買一件商品,使用大括號來調用也是沒有問題的
myShop{ println("I wanna buy a pen") }
不管是 runInThread 函數還是我們的 myShop 函數,最重要的都是把傳進來的代碼塊執行一次。有的同學可能會問:我為什么不直接單獨封裝我那一系列代碼塊,而非得復雜化成控制抽象來執行呢? 關於這點,我覺得如果您了解設計模式中的代理模式的話,可能就不會問出這個問題來了。附上一篇筆者多年前寫的博文: 大白話設計模式之代理模式
第 2 個例子
《快學 Scala 》 中第 2 個例子是一個應用到柯里化與遞歸調用的控制抽象示例。
1 def until (condition: => Boolean) (block: => Unit) { 2 if(!condition) { 3 block 4 until(condition)(block) 5 } 6 }
在有了上面第 1 個例子的理解以后,對於這個例子的函數定義,應該就沒什么問題的了。而關於這個函數的調用則是
1 var x = 10 2 until (x == 0) { 3 x -= 1 4 println(x) 5 }
相信這個函數調用,很多初學者都看的挺懵的,筆者也不例外。
這個例子書中說是模擬一個 while 表達式出來,看它的調用方式,也確實和 while 表達式一樣。但是我們的 until 函數明明是定義了兩個參數的,這里只傳一個真的合適嗎?
還是那句話:在理解了 Scala 中圓括號和大括號的作用以后,這段調用就顯得各種沒毛病了。這個調用中,確確實實是以柯里化的形式傳了兩個參數進去的。只不過第 1 個參數是以圓括號的形式來傳遞的,第 2 個參數由於是一系列的代碼塊,所以要用大括號的形式來傳遞。

花式柯里化調用
假如,我們在這個例子中的控制抽象函數只有一條語句的話,until 的調用形式完全可以改成下面這種更明朗的格式
1 var x = 10 2 until (x == 0)( x -= 1)
這是不是就很柯里化了?
The End
