又一篇寫給小師妹的Scala學習筆記·一

每天兩頁,分享《Programming in Scala》的心得。

這個系列之前其實寫過。具體可以看:

寫給小師妹的Scala學習筆記·開篇
寫給小師妹的Scala學習筆記·二

當時是爲了快速“學會”Scala,選擇了疾風式的搞法,幾周就擼完一本書。

這次我們慢下來,找一本Scala作者自己寫的書,仔細品一品Scala的設計哲學,並且以日記的形式記錄一下所思所想。

以下是正文部分。

2022-01-02

今天讀的這一章的大標題叫做“ A Scalable Language”。Scala這個名字,源於作者希望創造一種Scala-ble的語言。

接下來,他簡要的介紹了Scala的幾個特性:

首先,它是一種運行在JVM上的語言,因此和Java具有“無縫”的交操作性。藉助於Java強大的生態,可以少造無數輪子。當然像Groovy、Clojure等等都有這樣的特性。

其次,Scala兼有面向對象和函數式2種編程範式(這一點其實是相當不常見的),而且是一門靜態類型的語言(所謂動態一時爽,重構火葬場,靜態語言,可以讓編譯器替你幹不少髒活累活)。函數式的一面使得它易於構建小組件,面向對象使得它可以用來構建大型程序。

接下來作者強調,編寫Scala代碼的過程是“fun”的。並通過一個Map的例子,作者展示了,Scala具備類型推導能力和易用的API。

var captial = Map("US" -> "Washington", "France" -> "Paris")
captial += ("Japan" -> "Tokyo")
println(captial("France"))

比如,不需要多餘的分號,不需要聲明Map<String, String>這樣的類型,初始化時可以直接設置2個鍵值對進去,而不用單獨調用put方法。

對比一下原生Java的寫法:

Map<String, String> captial = new HashMap<>();
captial.put("US", "Washington");
captial.put("France", "Paris");

captial.put("Japan", "Tokyo");
System.out.println(captial.get("France"));

2022-01-03

今天讀的這一節的小標題是“A language that grows on you”,翻譯過來應該是“一門會讓你慢慢愛上的語言”。

接下來作者用兩個例子,印證了這句話。這兩個例子分別是,可自定義的數據類型(以BigInt爲例)和可自定義的控制結構(以Akka的API爲例)。在一番code show之後,作者都要表達一下“這些並不是語言內建(build-in)的特性,但是用起來和內建的沒有區別”。

過程中,還引用了Eric Raymond《大教堂與集市》的說法,表達了Scala的設計哲學更接近於集市。

到這裏,作者想表達的意思是比較明確的:Scala是一門內核極其精煉的語言,但卻有着非常高的可拓展性。就好像集市一樣,它並沒有(也不可能)事先規劃好所有的內容,但卻擁有極強的演化能力。

2022-01-04

今天讀的這一小節的標題是:“What makes Scala scalable?”。對於之前提到的面向對象及函數式編程再次做了個補充,並且可以說是乾貨滿滿。

首先作者講了,Scala是OO的。而OO的本質是把數據和操作封裝在一個容器中,這個容器就叫做Object。這樣,操作變成了一種數據,容器本身也作爲數據可以被傳來傳去。

同時,作者舉了例子,說有些語言不是那麼“純”的OO,比如在Java裏面,原始類型就不是對象(數組也不是),同時,Java還允許在class中定義靜態的字段和方法。而在Scala中,任何的值本質上都是對象,甚至與1 + 2也是,它的底層是針對1這個Int調用了+這個方法,同時傳入了參數2。

作者舉的第二個例子是關於trait。它有點像Java中的接口,但是擁有自己的字段和方法實現。關於trait其實一直有一些困惑,希望後面的章節可以解答。

接着作者又講到Scala是函數式的。這一段堪稱“教科書”式的介紹。

作者介紹了,函數式編程的2個特點。函數是一等公民、函數調用需要做到引用透明,也就是沒有副作用。

一等公民是指,函數可以在任意地方被定義(比如在一個函數裏),還可以像數值一樣作爲入參或者返回(高階函數)。

那什麼叫有副作用的函數調用。包括:

打印日誌、修改了函數的入參、從函數參數外(全局變量、bean、threadlocal)獲取數據、拋出異常等等。對應的,一個沒有副作用的,引用透明的函數,只有輸入/輸出,並且輸出可以被等價替換掉。

比如1+sum(1, 1)中,sum函數如果可以直接用2替換,就說明是引用透明的。

2022-01-05

今天的這一小節,大標題是“Why Scala?”,作者從兼容性、簡潔程度、高級抽象(?)和靜態類型4個方面講了,爲什麼要選擇Scala。

第一部分是兼容性,除了前面提到的互操作性之外,還強調了,像底層的String、Int之類的都是複用的Java原生的類型。同時,Scala還通過一個叫“隱式轉換”的概念,在不修改這些類源碼的情況下,對這些類做了增強。

第二部分是簡潔性。這個不多說了,差不多是同樣功能的Java的代碼量的1/4吧。後面又介紹了“trait”這個留到後面展開。

2022-01-08

中間的記錄斷了兩天,不過問題不大。書還是在看的。

這一段,作者提到Scala是“high-level”的,實際上,作者想強調的依然是“函數式”編程範式帶來的好處。

(正本清源)

函數式編程和麪向對象編程並不衝突,用Java照樣可以寫出非常函數式的代碼。

真正不太兼容的其實是描述式的風格和命令式的風格。

比如給定一個Int列表,求各元素之和。如果用命令式風格來寫,起手一個sum = 0,後接一個i = 0,最後for循環收尾。

如果用描述式風格來寫,“一個列表的各個元素之和” 等於 “列表的第一個元素” 加上 “列表其餘元素構成的列表的各個元素之和”。

假設取列表的第一個元素用head表示,取列表的剩餘元素構成的列表用tail表示。則僞代碼如下:

sum(alist) = {
if(alist != empty) return 0
else return head(alist) + sum(tail(alist))
}

可以看到,描述式風格和遞歸是比較自然的一對組合,無怪於FP系的語言都喜歡用遞歸。

遵循函數式風格的另一個好處,是可以寫出“引用透明”的函數。也就是說,這個函數調用的結果,可以用該函數的返回值等價的替換掉。

比如代碼裏有一段是:a + sum(b, c),如果b和c分別等於2和3的話,那麼和直接寫a + 5是等價的。或許因爲這個例子太簡單了,並且是數值計算,所以大部分人可以天然的寫出這種引用透明的函數。

但,如果是一段業務代碼,你能保證不在函數裏調用Spring的bean去讀寫數據庫嗎?能保證不修改某個全局變量或者ThreadLocal嗎?能保證不去調用某個入參的set方法嗎?

引用透明的另一種說法是“沒有副作用”,以上列的一些例子都是副作用的體現。

沒有副作用的代碼,易於測試和重構,也更少的引入bug。想想是不是經常發現某個字段,在經歷了一系列的函數調用之後,不知道什麼時候就被設置了一個不太符合預期的值,然後引出一系列莫名其妙的問題?

上面提到的最後一個例子,有一個專門的名字,叫aliasing problem,那麼函數式的編程風格,是如何避免這樣的問題呢?答案就是使用不可變的數據(immutable data)。

通過把一個類所有的字段都設置成final的,這個類就是一個不可變的類(如果有個字段是final的Map,非要去修改這個Map,就屬於硬槓了)。

相應的,函數體裏面也不再能set各種字段。一旦要改變些什麼,要應該通過返回一個新的類的實例完成。

實際上對於大多數程序員來說,疑問都是,不可變的數據,能編程嗎?看看Java的String,看看Spark的RDD。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章