Scala簡明語法

想看看SparkSQL的代碼,發現跟中學看英文小說一樣,只能模糊的猜測語言的含義,無法理解,更不要說欣賞。雖然一直對各種新的編程語言不太感冒,也不是很喜歡對一個基本概念反覆包裝然後還要定下規則,不過也沒辦法,還是得稍微了解下,否則這個代碼怕看不下去。

大概就是比較快速的把[1]裡面的內容看一遍,基於C++理解一下,便於後面看代碼,目標明確,內容簡單,只是為了加深印象,避免看代碼時候對著符號發獃:)

看了 @Glavo 評論認識到該文可能讓想學習scala的同學有錯誤的認識,非常抱歉。這裡要說明下,這個文章只是C++程序員想看Scala代碼而整理的一點信息,目標是便於對比記憶,迅速開始看代碼學習思想,用來學習Scala是不嚴謹的。對於Glavo提出的錯誤,我在文後分別查閱訂正,原文保持不變。謝謝Glavo同學。

Scala來自scalable,是面向對象和函數編程的混合體,靜態類型,易擴展。Scala編譯後的文件可以運行在JVM上,跟Java可以互操作。

  1. 有值和變數之分。值定義類似val x = 4 + 1, 一旦賦值不可改變,調用時候也不重新計算。變數定義類似var x = 1,可以重新賦值,支持lazy計算。類型會自動推導,也可以顯式定義,val/var x : Int = 4 + 1。比起Int x = 4,這似乎更符合自然語言的風格
  2. Block跟C++裡面一片代碼類似,block最後一個表達式的值就是該block的返回結果,省掉了return,也避免了返回類型聲明。Scala沒有return關鍵字
  3. 函數function是表達式的集合(帶參數的block),參數要指定類型,返回值類型自動推導。定義方式類似function(x : Int) => { x + 1 },也可以定義匿名函數。為了滿足強迫症患者,有些間接的方法可以顯式指定返回值類型,比如val x = new ((Int) => Int) { def apply(x : Int) = { x * 4 } } [3]。這裡的=>可以理解為聲明函數/方法時候的返回類型定義,而如果是定義函數/方法是參數後加冒號:
  4. 方法跟函數類似,不過方法可以很容易的指定返回類型,也可以有多個參數列表集合。def m(x : Int)(y : String) : Int = { ... }
  5. 類class跟C++類似,可以指定構造參數列表,返回Unit表示void,也可以寫作(),通過new來創建。參數中x : Int = 5表示默認值,調用時候如果顯式指定變數名則可以不遵守聲明的變數順序(一堆int參數的方法有救了)。成員默認public(不是很安全),可以添加private來限制,私有成員通過x_=()來實現getter/setter方法(寫兩個字母不好嗎?)。通過類構造器參數裡面聲明val/var聲明誰是成員變數,比如class Point(val x: Int = 0, var y: Int = 0)表示類裡面x,y是成員變數,但是x不能修改,y可以,也可以對y加上private[this] ,這樣實例就不能外面調用,或者加上final,子類不可見,這對結構體來說太方便了,N行代碼變一行。支持泛型,比如class X[A],可以在實例化或者繼承時候指定A的具體類型。不能多繼承。整體感覺類定義變簡單了,很多成員變數的屬性在參數裡面就指定了
  6. case class是一類特殊的class,方便實現switch case,創建後一般就不變了,可以不用new關鍵詞創建。case class比較值,不是引用,也許是調用變數的equal方法實現的。默認淺拷貝,可以顯示調用copy深拷貝。標準用法x match { x => t
    _ => default },下劃線有點像佔位符,在一些不需要參數的場景也是使用下劃線。在C++裡面為了可讀性會為一些枚舉定義ToString,使用case class會方便一些,比如ToString(enum) = val match { 1 => Bad }
  7. object是單例類,該類一旦定義好,該類的單例也就定義好了,比起C++裡面的實現,節省了幾行並發實例化的代碼。do方法是保留方法
  8. trait是一個很好的特性,可以通過with關鍵字實現多繼承(叫做mix-in),AOP(Aspect oriented programming)特性。函數式語言中一般有類似機制,大致等效於將對象上的函數摘除、重寫或者添加一個新函數。traits通過extends實現繼承(我納悶咋不用符號了,Scala不是很喜歡符號嗎?),通過override實現方法重載(比C++清晰),沒有構造函數,從類繼承時候也不必實現抽象方法。其和class的區別,這個哥們說的不錯「Conceptually, a trait is a component of a class, not a class by itself. As such, it typically does not have constructors, and it is not meant to stand by itself. I suggest using an abstract class when it has an independent meaning, and traits when you just want to add functionality in an object-oriented manner. " [4]。traits本身不能被實例化,但是通過new Traits{}編譯器會構造匿名類從而簡化開發。
  9. scala 程序入口是Main。object Main { def x(): Unit = println("Hello, Scala developer!") },通過Main.x()調用
  10. scala類型系統類似C++,Int/Char等是值,String等是對象,Any類型內建到語言層面,寫代碼應該會方便。類型關係見最後的圖,來自[1]
  11. 繼承或者組合:根據extends,with的順序逐步初始化內部變數,就好像調用了每個class或者traits的Init方法一樣
  12. 高階函數:類似closure遞歸綁定,參數還可以是closure,可以是獨立函數也可以是類實例的方法,支持泛型,指定或者推導均可
  13. currying, 如果一個方法有N個參數,調用時候參數M少於N,則自動形成一個函數,其參數列表就是剩下的N-M個參數,跟bind裡面的佔位符類似
  14. s"$var"可以自動拼接字元串,如果var是非字元,可以自動轉換
  15. apply和unapply:apply類似從參數構造對象,unapply反過來,可以從對象得到參數,比如var Class(para) = new Class(para1), 則para是可以被解析為para1的,作用似乎不大,變數本身也可以訪問,而且更清楚
  16. 關於variance 還是挺有用的,比如A <- B <- C繼承關係,container[B] 表示只能放B,不能放A C,叫做invariance;container[+B] 表示能放C,不能放A, 叫variance;container[-B] 表示能放A,不能放C,這個比較詭異,似乎沒啥用, 叫contrvariance
  17. 上面variance的約束只針對鄰居,如果是有龐大的繼承鏈,那麼需要更靈活的約束,通過<: 和 >:可以控制繼承鏈上的哪一段放進去。比如A <- B <- C <- D <- E,container[P <: B] 則只能B的基類才能放進來, 比如append(A);container[P >: B] 則只能B的子類才能放進來,比如append(C)
  18. Inner class:跟C++不同,inner class默認只屬於某個對象,而不是屬於定義這個inner class的類。class A { class IA { def x(n : IA) { ... } } },默認情況下,x接受的類型必須屬於某個對象,也只有這個對象創建的IA才能被接受;如果想越過這個規則,做類似C++ inner class的事情,則需要顯式定義,class A { class IA { def x(n : A#IA) { ... } } }。給人的感覺是默認情況下IA是A對象形成後才存在的
  19. 函數參數可以是組合類型,即def cloneAndReset(obj : Cloneable with Resetable) : Cloneable = { ... },只有這麼寫才能約束參數類型。複雜一點的寫法是先定義一個類型,在用這個類型作為參數
  20. Self-type:如果trait A裡面引用了trait B,那麼所有extends A的trait必須with B,在定義組合類行為時候可能更乾淨明確。這麼做大概是因為trait之間約束比較弱,這種弱可能是故意的,從而為trait之間的自由組合打下基礎。[10]裡面說到了基於此做依賴注入,跟C++功能類裡面聲明一個基類指針,然後使用時候賦值一個子類有點類似,儘可能獨立方便的增加系統靈活性。
  21. By-Name parameter,與By-Value parameter相對,後者只evaluate一次,後面直接使用結果,而前者每次調用都要重新evaluate。寫法類似def calculate(input: => Int) = input * 37,在類型前面加=>

對於分散式系統來說,我覺得一個語言是否好用可能有兩個方面比較重要,一個是並發模型,一個是異常體系。這一塊Scala可以選擇跟Java類似(Java也沒寫過幾行,看過一些),比如線程池和try..catch..final,也可以選擇Akka,並發處理和異常處理自成體系,Akka的文檔寫的不錯,從why到how都講了。不過我覺得還是把協程搞搞好,然後開發連Actor也不用看了,無限執行體,順序寫代碼還是很美好的。

Scala語法差不多也就這樣了,整體看,似乎想將代碼片段變得更短,從而看起來簡潔,也更加容易復用,很愛使用各種符號,看的我有點眼花。

圖1. 類型體系

關於原文錯誤的一些訂正(來自評論區,一併感謝)

  1. Unit是類型,來自AnyVal(見圖),()是值,隨意代指不嚴謹
  2. 6裡面的case代碼,每行都缺少了打頭case關鍵詞
  3. val和var:val賦值之後,變數不能被更換指向另外的值(但是不是說指向的值不能被改變,如果指向對象,內部狀態是可以被改變的);var賦值之後,變數可以被更換賦予新值。強調變數本身能否被替換,不強調指向目標是否可以被更改。快速理解可以類比C++裡面的const,但是也不對等。參考[5]
  4. 關於main,scala可以寫一個main函數,也可以extends App(此時可以直接使用args,來自App)。可以類比C++入口函數,此方法可以從命令行接收傳入參數。參考[6]
  5. 關於Currying,簡單理解如下,函數靠連續的()將參數分割為幾個組,後面調用如果少傳遞了一組或者幾組參數,這時候可以定義新的函數(操作函數像操作數據),比如def N(x:Int)(y:Int)(t:Int) = { println(x*y*t) }; var v = N(1)(_); v(3)(4),輸出12。var v等效於定義了y和t作為參數的函數。這裡體現了函數一等公民的身份,按照[7]裡面的解釋,currying本來就是層層函數定義出來的,這麼用只是正常的賦值而已
  6. apply/unapply機制可以輔助匹配,包括類型匹配,甚至對象實例裡面的元素匹配,比如在SQL解析之後可以用來比較兩個AST是否一樣。如果在C++裡面實現類似功能,可以重載operator==自己逐個比較對象元素。參考[8]
  7. 關於covariance/contravariance,前者可以使用容器理解,基類容器裡面可以放子類;後者可以使用函數理解,能處理基類的函數也能處理子類,所以在接收子類處理函數的地方可以使用基類處理函數。關於後者,將函數理解為數據,也是一種類型,容易理解一些。參考[9]

這些大體印象在後面看代碼過程中,可能繼續修正。

[1]. docs.scala-lang.org/tut

[2]. tutorialspoint.com/scal

[3]. familylifemag.com/quest

[4]. stackoverflow.com/quest

[5]. What is the difference between a var and val definition in Scala?

[6]. How to launch a Scala application with an object (main, app)

[7]. Scala 函數柯里化(Currying)

[8]. Scala 模式匹配 | 菜鳥教程

[9]. Contravariance vs Covariance in Scala

[10]. Real-World Scala: Dependency Injection (DI)


推薦閱讀:

有沒有一本講解gpu和CUDA編程的經典入門書籍?
Python 在編程語言中是什麼地位?為什麼很多大學不教 Python?
為什麼中國大學MOOC網課程結束後就看不了了?
如何系統的學習動態語言的類型推導,類型系統等知識?
程序員都是怎麼記代碼和編程語言的?

TAG:编程语言 | Spark | Scala |