如Scala官網宣稱的:“Object-OrientedMeetsFunctional”,這一句當屬對Scala最抽象的精準描述,它把近二十年間大行其道的面向對象編程與舊而有之的函數式編程有機結合起來,形成其獨特的魔力。希望通過本文能夠吸引你去了解、嘗試Scala,體驗一下其獨特魅力,練就自己的寒冰掌、火焰刀。
回首初次接觸Scala,時光已忽忽過去四五年。從當初“Scala取代Java”的爭論,到今天兩者的相安無事,Scala帶給了我們哪些有意義的嘗試呢?在我掌握的衆多編程語言之中,Scala無疑是其中最讓我感到舒適的,如Scala官網宣稱的:“Object-OrientedMeetsFunctional”,這一句當屬對Scala最抽象的精準描述,它把近二十年間大行其道的面向對象編程與舊而有之的函數式編程有機結合起來,形成其獨特的魔力。不知你是否看過梁羽生的著作《絕塞傳烽錄》?裏面白駝山主宇文博的絕學:左手“寒冰掌”、右手“火焰刀”,用來形容Scala最爲合適了,能夠將OOP與FP結合得如此完美的語言,我認爲唯有Scala。
衆所周知,Java稱不上純粹的面嚮對象語言,但Scala卻擁有純粹的面向對象特性,即便是1+1這麼簡單的事情,實際上也是執行1.+(1)。而在對象組合方面,Scala擁有比接口更加強大的武器──特質(trait)。
Scala同時作爲一門函數式編程語言,理所當然地具備了函數式語言的函數爲頭等“公民”、方法無副作用等特性。事實上,Scala更吸引我的並不是OOP特性,而是FP特性!一邊是OOP、一邊是FP,這就是多面的Scala,極具魅力而且功能強大。
在多核時代,現代併發語言不斷涌現出來,例如Erlang、Go、Rust,Scala當然也位列其中。Scala的併發特性,堪稱Scala最吸引開發者的招牌式特性!Scala是靜態類型的。許多人會把vals="ABC"這樣的當作動態類型特性,而vals:String="ABC"才認爲是靜態類型特性。實際上,這無關類型爭論,而是類型系統實現的範疇。是的,在Scala裏,你可以放心大膽地使用vals="ABC",而Scala裏強大的類型推斷和模式匹配,絕對會讓你愛不釋手。
此外,Scala作爲JVM語言,理所當然享有Java龐大而優質的資源,與Java間可實現無縫交互,事實上,Scala最終當然是編譯爲Java字節碼。
本文將把重點放在Scala的特色之處。作爲一門完備而日趨成熟的語言,Scala的知識點有不少,本文當然無法做到面面俱到,但希望能夠帶你感受Scala魅力,並理解其重要概念。
Scala的面向對象
開胃菜──類的定義
來看個開胃菜,定義一個類:
我們知道,動態語言一般都提供了REPL環境,同時,動態語言的程序代碼都是以腳本方式解釋運行的,這給開發帶來了不少的便利。Scala雖然是靜態類型系統的語言,但同樣提供了這兩個福利,讓你倍感貼心。
因此,你可以任意採取以下運行方式:
- 在命令行窗口或終端輸入:scala,進入Scala的REPL窗口,逐行運行上述代碼;
- 此外,也可以將上述代碼放入某個後綴名爲.scala的文件裏,如test.scala,然後通過腳本運行方式運行: scala test.scala。
測試信息“小強今年32歲,是一名程序員”結果出來了!
多麼簡單,類的定義就這麼多,卻能夠做這麼多事情,想想Java的實現吧,差別太大了。我們先來分析下代碼。假設在上述第二種方式的test.scala文件中,註釋掉後面兩行並保存,運行:
- scalac test.scala
- javap -p Person
這個結果跟Java實現的代碼類似(生成的getter和 setter跟Java實現有所不同,但在這裏不是什麼問題),可見,Scala幫我們做了多少簡化工作。這段代碼有以下值得注意的地方:
我們可以把字段定義和構造函數直接寫在Scala的類定義裏,其中,關鍵字val的含義是“不可變”,var 爲“可變”,Scala的慣用法是優先考慮val,因爲這更 貼近函數式編程風格;
- 在Scala中,語句末尾的分號是可選的;
- Scala默認類訪問修飾符爲public;
- 注意println("測試信息")這一行,將在主構造函數裏執行;
- val與var兩者對應Java聲明的差異性已在反編譯代碼中體現了。
伴生對象與伴生類在Scala的面向對象編程方法中佔據極其重要的位置,例如Scala中許多工具方法都是由伴 生對象提供的。
伴生對象首先是一個單例對象,單例對象用關鍵字object定義。在Scala中,單例對象分爲兩種,一種是並未自動關聯到特定類上的單例對象,稱爲獨立對象 (Standalone Object);另一種是關聯到一個類上的單例對象,該單例對象與該類共有相同名字,則這種單例對象稱爲伴生對象(Companion Object),對應類稱爲伴生類。
Java中的類,可以既有靜態成員,又有實例成員。而在Scala中沒有靜態成員(靜態字段和靜態方法),因爲靜態成員從嚴格意義而言是破壞面向對象純潔性的,因此,Scala藉助伴生對象來完整支持類一級的屬 性和操作。伴生類和伴生對象間可以相互訪問對方的 private字段和方法。
接下來看一個伴生類和伴生對象的例子(Person. scala)。
這是一個典型的伴生類和伴生對象的例子,注意以下說明:
- 伴生類Person的構造函數定義爲private,雖然這不是必須的,卻可以有效防止外部實例化Person類,使得Person類只能供對應伴生對象使用;
- 每個類都可以有伴生對象,伴生類與伴生對象寫在同一個文件中;
- 在伴生類中,可以訪問伴生對象的private字段Person.uniqueSkill;
- 而在伴生對象中,也可以訪問伴生類的private方法 Person.getUniqueSkill();
- 最後,在外部不用實例化,直接通過伴生對象訪問Person.printUniqueSkill()方法。
Scala的特質類似於Java中的接口作用,專門用來解決現實編程中的橫切關注點矛盾,可以在類或實例中混入(Mixin)這些特質。實際上,特質最終會被編譯成Java的接口及相應的實現類。Scala的特質提供的特性遠比Java的接口靈活,讓我們直接來看點有趣的東西吧。
我們先是定義了一個Programmer抽象類。最後定義了四個不同程序員的Trait,且都繼承自Programmer抽象類,然後,通過不同的特質排列組合,看看我們產生的結果是什麼樣子的:
所有程序員都至少掌握一門編程語言。
我掌握Scala。我掌握Golang。
所有程序員都至少掌握一門編程語言。
我掌握Scala。我掌握Golang。我掌握PHP。......
Wow~!有趣的事情發生了,通過混入不同的特質組合,不同的程序員都可以有合適的詞來介紹自己,而每個程序員的共性就是:“所有程序員都至少掌握一門編程語言”。讓我們來解釋一下具體思路:
這段代碼裏面,特質通過with混入實例,如:new Programmer with Scalaist。當然,特質也可以混入類中;
- 爲什麼信息可以傳遞呢?比如我掌握Scala。我掌握Golang。我掌握PHP?答案就在super.getSkill()上。該調用不是對父類的調用,而是對其左邊混入的Trait的調用,如果到左邊第一個,就是調用Programmer抽象類的getSkill()方法。這是Trait的一個鏈式延時綁定特性,那麼在現實中,這個特性就表現出極大的靈活性,可以根據需要任意搭配,大大降低代碼量。
Scala的面向對象特性,暫先介紹到這裏。其實還有好些內容,限於篇幅,實在是有點意猶未盡的感覺。
Scala的函數式風格
Scala的魅力之一就是其函數式編程風格實現。如果把上面介紹的面向對象特性看成是Scala的“寒冰掌”,讓你感受到了迥異於Java實現的特性,那麼,Scala強大而魔幻的函數式特性,就是其另一大殺招“火焰刀”,噴發的是無堅不摧的怒焰之火。
集合類型
Scala常用集合類型有Array、Set、Map、Tuple和List等。Scala提供了可變(mutable)與不可變(immutable)的集合類型版本,多線程應用中應該使用不可變版本,這很容易理解。
- Array:數組是可變的同類對象序列;
- Set:無序不重複集合類型,有可變和不可變實現;
- Map:鍵值對的映射,有可變和不可變實現;
- Tuple:可以包含不同類元素,不可變實現;
- List:Scala的列表是不可變實現的同類對象序列,因應函數式編程特性的需要。
- List大概是日常開發中使用最多的集合類型了。
這些集合類型包含了許多高階函數,如:map、find、filter、fold、reduce等等,構建出濃郁的函數式風格用法,接下來我們就來簡單瞭解一下:
輸出如下:
JavaScript很棒~
Scala很棒~
Golang很棒~
map()函數在List上迭代,對List中的每個元素,都會調用以參數形式傳入的Lambda表達式(或者叫匿名函數)。其結果是創建一個新的List,其元素內容都發生了相應改變,可以從輸出結果觀察到。注意,代碼中有一行是速寫法代碼,我個人比較喜歡這種形式,但在複雜代碼中可讀性差一些。
最後,我們用了另一個foreach()方法來迭代輸出結果。
高階函數、Lambda表達式,都是純正的函數式編程風格。如果你接觸過Haskell,就會發現Scala函數式風格的實現,在骨子裏像極了Haskell,感覺非常親切。在編寫Scala代碼的過程中,將處處體現出它的函數式編程風格,高效而簡潔。
限於篇幅,我們只能淺嘗輒止,如果有興趣,可以進一步參考我以前寫的兩篇相關博文,裏面有比較詳細的描述:七八個函數,兩三門語言㈠和七八個函數,兩三門語言㈡•完結篇。
高階函數、柯里化、不全函數和閉包
實際上我們在前面已經見識過Scala的高階函數(Higher-order Function)了,只不過是Scala自帶的map()和foreach()。高階函數在維基百科中的定義 是:“高階函數是至少滿足下列一個條件的函數:接 受函數作爲輸入;輸出一個函數”。接下來,我們來實現一個自己的高階函數──求圓周 長和圓面積:
我們定義了一個高階函數cycle。輸入參數中傳入一個函數值calc,其類型是函數,接收Float輸入,輸出也是Float。在實現裏,我們會調用calc函數。在調用時,我們分別傳入求圓周長和圓面積的匿名函數,用於實現calc函數的邏輯。
這樣,我們用一個高階函數cycle,就可以滿足求圓周長和圓面積的需求,不需要分別定義兩個函數來處理不同任務,而且代碼直觀簡潔。最後,我們打印結果,輸出一組半徑分別對應的圓周長和圓面積。在這裏,我們用到了映射Map:
圓周長:Map(1.0 -> 6.28, 2.3 -> 14.444, 4.5 -> 28.26)
圓面積:Map(1.0 -> 3.14, 2.3 -> 16.6106, 4.5 -> 63.585)
接下來,我們對上述代碼稍加改動:
輸出結果同上。
注意到了嗎?我們把cycle函數的兩個輸入參數進行了拆分(如上述代碼第一行),同時在調用cycle函數時,方式也有所不同(如上述代碼最後兩行)。這是什麼意思?
這在函數式編程中稱爲柯里化(Curry),柯里化可以把函數定義中原有的一個參數列表轉變爲接收多個參數列表。在函數式編程中,一個參數列表裏含多個參數的函數都是柯里函數,可以柯里化。
要知道,在函數式編程裏,函數是一等的,當然函數也可以作爲參數和返回被傳遞。這對初次接觸函數式編程的開發者而言確實比較抽象。上述代碼的理解,你可以這樣想象:(cacl: Float => Float)是函數cycle2(r: Array[Float])的輸入參數!進一步,可以這麼理解:cacl取一個參 數,變成了一個不全函數(Partially Function)cycle2 (r: Array[Float]),所謂不全函數就是它還有參數未確定,你想要完整用它的話,還需要繼續告知它未定的 參數,如(cacl: Float => Float)。
還沒完!根據上述描述,我們繼續看看如何用各種Hacker的調用方式:
可以用valc21=cycle2 _、val c22 = cycle2(Array (1.0f, 2.3f, 4.5f)) _諸如此類的方式創建不全函數,並調用它。
看得出來,不全函數同樣可以提升代碼的簡潔程度,比如本例代碼中,參數Array(1.0f, 2.3f, 4.5f)是固定不 變的,我們就不用每次都在調用cycle2時傳入它,可以 先定義c22,再用c22來處理。
函數式崇尚的“函數是第一等公民”理念可不容小覷。函數,就是這麼任性!接下來,我們來了解下閉包(Closure)的概念,依舊先看個簡單的例子:
這個例子用來求圓柱體的體積。這裏定義了一個caclCylinderVolume函數(因爲函數式風格里函數是一等公民,所以可以用這樣的函數字面量方式來定義。或者也可以稱之爲代碼塊),函數裏面引用了一個自由變量high,caclCylinderVolume函數並未綁定high。而在caclCylinderVolume函數運行時,要先“閉合”函數及其所引用變量high的外部上下文,這樣也就綁定了變量high,此時綁定了變量high的函數對象稱爲閉包。
由代碼可知,由於函數綁定到了變量high本身,因此,high如果發生改變,將影響函數的運算結果;而如果在函數裏更新了變量,那這種更新在函數之外也會被體現。
模式匹配(PatternMatching)
Scala的模式匹配實現非常強大。模式匹配爲編程過程帶來了莫大便利,在Scala併發編程中也得到了廣泛應用。
輸出結果如下:
多面者Scala~
你的Scala版本是:2.11.6
八成是乾淨簡潔的Go、PHP語言呢?
可見,模式匹配特性非常好用,可以靈活應對許多複雜的應用場景:
- 第一個case表達式匹配普通的字面量;
- 第二個case表達式匹配正則表達式;
- 第三個case表達式使用了if判斷,這種方式稱爲模式護衛(Pattern Guard),可以對匹配條件加以過濾;
- 第四個case表達式使用了“_”來處理未匹配前面幾項的情況。
此外,Scala的模式匹配還有更多用法,如case類匹配、option類型匹配,同時還能帶入變量,匹配各種集合類型。綜合運用模式匹配,能夠極大提升開發效率。
併發編程
現代語言的特性往往是隨硬件環境和技術趨勢演進的,多核時代的來臨,互聯網大規模複雜業務處理,都對傳統語言提出了挑戰,於是,新展現的語言幾乎都非常關注併發特性,Scala亦然。
Scala語言併發設計採用Actor模型,借鑑了Erlang的Actor實現,並且在Scala2.10之後,改爲使用AkkaActor模型庫。Actor模型主要特徵如下:
- “一切皆是參與者”,且各個actor間是獨立的;
- 發送者與已發送消息間解耦,這是Actor模型顯著特點,據此實現異步通信;
- actor是封裝狀態和行爲的對象,通過消息交換進行相互通信,交換的消息存放在接收方的郵箱中;actor可以有父子關係,父actor可以監管子actor,子actor唯一的監管者就是父actor;
- 一個actor就是一個容器,它包含了狀態、行爲、一個郵箱(郵箱用來接受消息)、子actor和一個監管策略。
我們先來看個例子感受下:
在這裏,Concurrency是CalcActor的父actor。在Concurrency中先要構建一個Akka系統:
同時,這裏的設置將會在線程池裏初始化稱爲“routee”的子actor(這裏是CalcActor),數量爲4,也就是我們需要4個CalcActor實例參與併發計算。這一步很關鍵。actor是一個容器,使用actorOf來創建Actor實例時,也就意味着需指定具體Actor實例,即指定哪個actor在執行任務,該actor必然要有“身份”標識,否則怎麼指定呢?!
在Concurrency中通過以下代碼向CalcActor發送序號並啓動併發計算:
for(i<-1to4)calcActor!i
然後,在CalcActor的receive中,通過模式匹配,對接收值進行處理,直到接收值處理完成。在運行結果就會發現每次輸出的順序都是不一樣的,因爲我們的程序是併發計算。比如某次的運行結果如下。
- 序號爲:1。
- 序號爲:3。
- 序號爲:2。
- 序號爲:4。
actor是異步的,因爲發送者與已發送消息間實現瞭解耦;在整個運算過程中,我們很容易理解發送者與已發送消息間的解耦特徵,發送者和接收者各種關心自己要處理的任務即可,比如狀態和行爲處理、發送的時機與內容、接收消息的時機與內容等。當然,actor確實是一個容器,且五臟俱全:我們用類來封裝,裏面也封裝了必須的邏輯方法。Akka基於JVM,雖然可以穿插混合應用函數式風格,但實現模式是面向對象,天然講究抽象與封裝,其當然也能應用於Java語言。我們的Scala之旅就要告一個段落了!Scala功能豐富而具有一定挑戰度,上述三塊內容,每一塊都值得擴展詳述,但由於篇幅關係,在此無法一一展開。
希望通過本文能夠吸引你去了解、嘗試Scala,體驗一下其獨特魅力,練就自己的寒冰掌、火焰刀。