戲路如流水,從始至終,點(diǎn)滴不漏。一路百折千回,本性未變,終歸大海。一步一戲,一轉(zhuǎn)身一變臉,撲朔迷離。真心自然流露,舉手投足都是風(fēng)流戲。一旦天幕拉開,地上再無演員。
Scala是由Martin Ordersky設(shè)計(jì)的一門混合「面向?qū)ο蟆购汀负瘮?shù)式」,并具備完備的「泛型編程」能力,支持多種范式的程序設(shè)計(jì)語言。
Scala取名為「可擴(kuò)展的語言」,因?yàn)樗鼡碛辛己玫膹椥?。它使用不變的語言內(nèi)核,構(gòu)建萬千變化的世界。同時,Scala也是設(shè)計(jì)「DSL(領(lǐng)域描述語言)」的利器,讓編程充滿輕松,愉快的氣氛,并富有成就感。
Scala擁有強(qiáng)大的類型系統(tǒng),具有豐富的表達(dá)能力,語法簡潔,優(yōu)雅,靈活。它應(yīng)用廣泛,從編寫簡單的腳本,到構(gòu)建大型的企業(yè)級系統(tǒng)。
Scala運(yùn)行于JVM之上,并與現(xiàn)有的JVM生態(tài)系統(tǒng)無縫鏈接,而無需定義額外的膠水代碼。它兼容既有Java的類庫,讓成千上萬的程序可以繼續(xù)工作,并能夠得以復(fù)用。
Scala的哲學(xué)
Scala的設(shè)計(jì)吸收了眾多程序設(shè)計(jì)語言優(yōu)秀的思想,取精除粕,形成了自身特有的哲學(xué)思維體系。
- 自由:釋放自由,方能創(chuàng)造奇跡;
- 復(fù)用:討厭重復(fù),重用既有代碼;
- 抽象:正交設(shè)計(jì),擁抱未來變化;
- 開放:對外擴(kuò)展開放,對內(nèi)拒絕修改;
- 友好:專家級的瑞士軍刀;
Scala的基因
Scala首先偏向Java社區(qū)的使用習(xí)慣,包括表達(dá)式,代碼塊,還有包和引用的等語法習(xí)性。而對于Java用戶唯一提出挑戰(zhàn)的就是「類型修飾」被放在變量后面了;但是,當(dāng)習(xí)慣了Scala代碼風(fēng)格后,你會發(fā)現(xiàn)「后置類型修飾」具有很多優(yōu)勢。
Scala借鑒了Smalltalk的「對象模型」,修正了Java對象模型存在的諸多不足。例如,在沒有損失性能的前提下,將AnyVal, AnyRef兩者完美統(tǒng)一起來;不僅考慮層次的頂端,還設(shè)計(jì)了層次的末端,例如,Nothing的抽象,對「類型推演」具有重大意義。
Scala也借鑒了Haskell「類型系統(tǒng)」的設(shè)計(jì),及其「函數(shù)式」的思維,并結(jié)合自身特性,優(yōu)雅地將OO和FP整合在一起,取長補(bǔ)短,極大地增強(qiáng)了Scala的威力。
Scala也借鑒了C++「多重繼承」,并吸收了Ruby的Mixin的特性,設(shè)計(jì)了強(qiáng)大的trait機(jī)制實(shí)現(xiàn)靈活的對象組合機(jī)制。
Scala也借鑒了Erlang的思想,在沒有改變內(nèi)核的情況下,通過擴(kuò)展類庫的方式支持actor的并發(fā)編程模式。
Scala也借鑒了C++語言的一些特性,例如「操作符重載」,「隱式轉(zhuǎn)換」等特性;尤其增強(qiáng)了的「隱式轉(zhuǎn)換」成為Scala可擴(kuò)展性的重要機(jī)制。
Scala的特質(zhì)
接下來,通過幾個簡單的例子,闡述Scala所具有的一些特點(diǎn),并闡述選擇Scala的動機(jī)。
Scala是自由的
No One Size Fits All.
Scala既增強(qiáng)了OO的語義,也引入了FP的思維,同時也擁有完備的「泛型編程」能力,它們互相截長補(bǔ)短,并融為一體。
Scala更像一把瑞士軍刀,支持多種編程范式。Scala程序員擁有豐富的工具箱,當(dāng)面對具體問題時擁有很大的自由空間,力求使用最簡單的方法解決問題。
需求:要定義一個「讀取器」,可以從字符中直接獲取,可以從文件中讀取,甚至從網(wǎng)絡(luò)
IO讀取。
為了得到一致的抽象,可以定義Reader的抽象體,并對「數(shù)據(jù)源」這一變化方向進(jìn)行分離。Scala是一門多范式的程序設(shè)計(jì)語言,這里嘗試使用兩種不同的思維嘗試解決這個問題。
類型參數(shù)
先定義泛型的Reader[+T],并賦予協(xié)變的能力。
trait Reader[+T] {
val source: T
def read: String
}
然后,再子類化一個StringReader,數(shù)據(jù)源從字符串中直接獲取。
class StringReader(val source: String) extends Reader[String] {
def read: String = source
}
再定義一個FileReader,數(shù)據(jù)源從文本文件中獲取。
class FileReader(val source: File) extends Reader[File] {
def read: String =
using(Source.fromFile(source)) { file =>
file.getLines.mkString(Properties.lineSeparator)
}
}
using是一個自定義的抽象控制結(jié)構(gòu),用于保證資源的安全釋放,它是「借貸模式」的經(jīng)典應(yīng)用,后文將對它進(jìn)行闡述。
抽象類型
也可以先定義一個抽象類型:type In;基于抽象類型In,再定義了一個抽象字段:val source: In;最后,Reader還定義了一個抽象方法read。
trait Reader {
type In
val source: In
def read: String
}
然后,再子類化一個StringReader,數(shù)據(jù)源從字符串中直接獲取。
class StringReader(val source: String) extends Reader {
type In = String
def read: String = source
}
如果數(shù)據(jù)源從文本文件中獲取,FileReader實(shí)現(xiàn)如下:
class FileReader(val source: File) extends Reader {
type In = File
def read: String =
using(Source.fromFile(source)) { file =>
file.getLines.mkString(Properties.lineSeparator)
}
}
Scala是抽象的
Scala對于控制系統(tǒng)的復(fù)雜度擁有強(qiáng)大的抽象能力。甚至具備「控制結(jié)構(gòu)」的抽象能力,使得設(shè)計(jì)更加正交,合理,程序更加簡單,優(yōu)雅。
需求1:判斷某個單詞是否包含數(shù)字
使用Java
可以使用迭代快速實(shí)現(xiàn)這個需求。
public static boolean hasDigit(String word) {
for (int i = 0; i < word.length(); i++)
if (Character.isDigit(word.charAt(i)))
return true;
return false;
}
需求2:判斷某個單詞是否包含大寫字母
當(dāng)然,可以通過復(fù)制粘貼,重復(fù)實(shí)現(xiàn)相同的邏輯;但是將導(dǎo)致明顯的重復(fù)設(shè)計(jì)。
public static boolean hasUpperCase(String word) {
for (int i = 0; i < word.length(); i++)
if (Character.isUpperCase(word.charAt(i)))
return true;
return false;
}
為了得到更為抽象的設(shè)計(jì),使得代碼具有高度的可復(fù)用性,可以提取一個抽象的CharacterSpec概念。
public interface CharacterSpec {
boolean satisfy(char c);
}
hasDigit, hasUpperCase合二為一,實(shí)現(xiàn)算法邏輯的代碼復(fù)用。
public static boolean exists(String word, CharacterSpec spec) {
for (int i = 0; i < word.length(); i++)
if (spec.satisfy(word.charAt(i)))
return true;
return false;
}
可以如下使用這個函數(shù),判斷單詞是否包含數(shù)字。
exists(word, new CharacterSpec() {
@Override
public boolean satisfy(char c) {
return Character.isDigit(c);
}
});
對于判斷是否包含大寫字母,則可以實(shí)現(xiàn)為:
exists(word, new CharacterSpec() {
@Override
public boolean satisfy(char c) {
return Character.isUpperCase(c);
}
});
但是,使用匿名內(nèi)部類,將導(dǎo)致復(fù)雜的程序結(jié)構(gòu)和冗余的語法噪聲。
使用Java8
可以使用Java8的lambda表達(dá)式簡化設(shè)計(jì)。
exists(word, c -> Character.isDigit(c));
如果使用「方法引用」,可以進(jìn)一步改善程序的表達(dá)力。
exists(word, Character::isDigit);
但是,即使使用Java8,設(shè)計(jì)依然還是美中不足。其一,exists擁有兩個參數(shù),如果能夠做到如下的「代碼塊」,那就太神奇了。
// 假設(shè)可以定義代碼塊
exists(word) { Character::isDigit };
其二,如果將exists成為String的一個方法,設(shè)計(jì)將更加具有OO的風(fēng)格了。
// 假設(shè)String擁有exists方法
word.exists(Character::isDigit);
可惜的是,Java并沒有上述的能力。
使用Scala
首先,Scala可以兼容既有的Java設(shè)計(jì),而無需付出額外的成本。按照慣例,對Java實(shí)現(xiàn)的StringUtil.exists可以做一個簡單的包裝,隱藏匿名內(nèi)部類的實(shí)現(xiàn)細(xì)節(jié),并對外提供「代碼塊」定制的風(fēng)格。
def exists(s: String)(p: Char => Boolean): Boolean =
return StringUtil.exists(s, new CharacterSpec {
override def satisfy(c: Char): Boolean = p(c)
})
也就是說,相比Java的實(shí)現(xiàn),Scala借助「柯里化」的機(jī)制,進(jìn)一步改善代碼的表達(dá)力。
exists(word) { _.isUpper }
事實(shí)上,Scala運(yùn)用「隱式轉(zhuǎn)換」的神奇魔法,可以將String的功能增強(qiáng)為StringOps,使其直接能夠調(diào)用exists方法。
word.exists(_.isUpper)
如果偏愛大括號,也可以寫成這樣:
word.exists { _.isUpper }
Scala是簡潔的
Scala極度討厭「重復(fù)」,嚴(yán)格堅(jiān)持DRY(Don't Repeat Youself)原則。不僅體現(xiàn)在語法上,還包括類庫的設(shè)計(jì)上。
需求:設(shè)計(jì)一個貨幣的值對象。
使用Java
Java實(shí)現(xiàn)「值對象」時,語法較為啰嗦,并具有相同的模式,很容易形成重復(fù)的「樣板代碼」。
例如使用private定義字段,并在構(gòu)造函數(shù)進(jìn)行初始化;然后定義字段的Getter接口;即使equals,hashCode等具有邏輯的方法時,也表現(xiàn)為固定的模式。
public class Currency {
private final int amount;
private final String designation;
public Currency(int amount, String designation) {
this.amount = amount;
this.designation = designation;
}
public String getDesignation() {
return designation;
}
public int getAmount() {
return amount;
}
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object obj) {
...
}
}
在Java社區(qū),也存在實(shí)用方法,或者類庫,支持equals, hashCode的自動生成,但也要讓程序員付出額外的努力。
使用Scala
Scala對于重復(fù)的事情從來不說兩次。對于固定的模式,擁有最直接、最簡潔的表達(dá)方式,從而大幅地削減了代碼量。
case class Currency(amount: Int, designation: String)
在Scala社區(qū),case class是定義「值對象」的最佳實(shí)現(xiàn)模式。它天然地?fù)碛?code>Getter, equals, hashCode等方法實(shí)現(xiàn),并且在其「伴生對象」中擁有apply的工廠方法。
Scala是性感的
Scala語法輕量,并具備豐富的表達(dá)力。借助于Scala強(qiáng)大的「類型推演」能力,Scala的簡潔程度可以和「動態(tài)語言」相媲美。
需求:建立一個電話簿的數(shù)據(jù)表格。
使用Java
使用Java建立一個簡單的電話簿數(shù)據(jù)表格。
Map<String, String> phonebook = new HashMap<String, String>() {{
put("Horance", "+9519728872");
put("Dave", "+9599820012");
}};
其中,參數(shù)類型<String, String>重復(fù)地聲明了兩次,構(gòu)造靜態(tài)表也使用了特殊的「初始化」的語義。
使用Scala
使用Scala,代碼實(shí)現(xiàn)不僅輕量,表現(xiàn)力也相當(dāng)不錯。
val phonebook = Map(
"Horance" -> "+9519728872"
"Dave" -> "+9599820012"
)
沒有冗余的類型噪聲,而且具有更形象的語法描述。更重要的是,->操作符并不是語言內(nèi)核所支持的,而是通過簡單地類庫擴(kuò)展而實(shí)現(xiàn)的。
也就是說,"Horance" -> "+9519728872"構(gòu)造了一個類型為Tuple2[String, String]的二元組,它等價(jià)于("Horance", "+9519728872")。
事實(shí)上,->定義在Predef中。
object Predef {
implicit final class ArrowAssoc[A](self: A) extends AnyVal {
def ->[B](y: B) = (self, y)
}
}
ArrowAssoc實(shí)現(xiàn)了self: A的功能增強(qiáng),使其擁有->方法,而該方法返回一個二元組。
Scala是多變的
Scala猶如變形金剛,擁有無窮變化的空間。Scala倡導(dǎo)一個問題,擁有多種解法的思維習(xí)慣。當(dāng)面對具體問題時,使得程序員擁有更多的自由選擇權(quán)。
需求:打印程序選項(xiàng)列表
使用var
Scala雖然倡導(dǎo)函數(shù)式的思維,但在某些性能苛刻的場景,指令式可能成為最后的救命稻草。
object Main extends App {
var i = 0
while (i < args.length) {
println(args(i));
i += 1
}
}
for推導(dǎo)式
object Main extends App {
for (arg <- args)
println(arg)
}
函數(shù)字面值
Scala的函數(shù)具有一等公民的地位,跟其他值一樣可以被傳遞和存儲。foreach就是一個高階函數(shù),它接受函數(shù)字面值作為參數(shù)進(jìn)行傳遞。
object Main extends App {
args.foreach((arg: String) => println(arg))
}
類型自動推演
object Main extends App {
args.foreach(arg => println(arg))
}
占位符
因?yàn)?code>arg在foreach中僅出現(xiàn)過一次,可以使用_的占位符簡化實(shí)現(xiàn)。
object Main extends App {
args.foreach(println(_))
}
部分應(yīng)用函數(shù)
也可以將println看成一個整體進(jìn)行傳遞。需要注意的是,println與_之間有且僅有一個空格。
object Main extends App {
args.foreach(println _)
}
對于此例,可以將傳遞println函數(shù)名,簡化實(shí)現(xiàn),提高表現(xiàn)力。
object Main extends App {
args.foreach(println)
}
可選的逗號和括號
在特殊場景下,逗號和括號是可選的,代碼的表現(xiàn)力猶如自然語言一樣直白。
object Main extends App {
args foreach println
}
Scala是開放的
Open for Extension.
Scala具有高度可擴(kuò)展性的架構(gòu),借助于trait,抽象類型和泛型,Self Type,隱式轉(zhuǎn)換等機(jī)制,使得它具備強(qiáng)大的靈活性。
以using的設(shè)計(jì)為例,講解Scala對于擴(kuò)展性的支持,及其設(shè)計(jì)內(nèi)部DSL的技術(shù),加深對Scala的理解。
形式化
對于資源管理,可以簡化為如下的數(shù)學(xué)模型:
Input: Given resource: R
Output:T
Algorithm:Call back to user namespace: f: R => T, and make sure resource be closed on done.
它表示:給定一個資源R,并將資源R傳遞給用戶空間,并回調(diào)算法f: R => T;當(dāng)過程結(jié)束時資源自動釋放。
using實(shí)現(xiàn)
import scala.language.reflectiveCalls
object using {
def apply[R <: { def close(): Unit }, T](resource: => R)(f: R => T): T = {
var source: Option[R] = None
try {
source = Some(resource)
f(source.get)
} finally {
for (s <- source)
s.close
}
}
}
using常常被稱為「借貸模式」,是保證資源自動回收的重要機(jī)制。
控制抽象
使得using形如內(nèi)置于語言的控制結(jié)構(gòu),其行為類似于if, while一樣。
def read: String = using(Source.fromFile(source)) { file =>
file.getLines.mkString(Properties.lineSeparator)
}
鴨子編程
R <: { def close(): Unit }用于約束R類型的特征,它必須擁有def close(): Unit方法。這是Scala支持「鴨子編程」的一個重要技術(shù)。
例如,File滿足R類型的特征,因?yàn)樗鼡碛?code>close方法。
惰性求值
resource: => R是按照by-name傳遞,在實(shí)參傳遞形參過程中,并未對實(shí)參進(jìn)行立即求值,而將求值推延至resource: => R的調(diào)用點(diǎn)。
例如,using(Source.fromFile(source))并沒有馬上調(diào)用fromFile方法,并傳遞給形參,而將求值推延至source = Some(resource)語句,即調(diào)用Some.apply方法時才展開計(jì)算。
Option
在Scala社區(qū),Option常常用于表示「存在與不存在」兩者之間的語義。它存在兩種形式:Some, None。
for推導(dǎo)式
在finally關(guān)閉資源時,使用for推導(dǎo)式過濾掉None。也就是說,如下三種形式是等價(jià)的。
- 過濾掉
None,并自動提取Option中的元素。
for (s <- source)
s.close
- 使用
if,但需要從Some中手動get。
if (source != None)
source.get.close
Scala的明天
軟件設(shè)計(jì)的目的就是為了控制復(fù)雜性,讓軟件應(yīng)對未來變化具有更好的彈性。Scala強(qiáng)大而自由,當(dāng)程序員設(shè)計(jì)一個應(yīng)用和類庫時,具有很大的自由空間。
但是,Scala過度的靈活性,往往會誘惑他人掉進(jìn)復(fù)雜性的深淵而不能自拔。它猶如具有「魔戒」的力量,雖然強(qiáng)大,但也很致命。
Complexity is like a bug light for smart people. We can't resist it, even though we know it's bad for us. -- Alex Payne.
因此,應(yīng)該理智地抵制復(fù)雜性的誘惑,才能真正地發(fā)揮Scala的威力。使用Scala不是為了炫技,而應(yīng)該盡最大的可能讓設(shè)計(jì)保持簡單。
Martin Ordersky也在2016年元旦之初發(fā)文,號召社區(qū)有志之士在未來的時間里盡最大可能地降低Scala的復(fù)雜度。
我堅(jiān)信,Scala的明天會更簡單,更加漂亮。