近來工作的重心完全轉到了數據工程上,幾乎一點網站相關的東西都沒有了。項目的主管老板受到他業界一位親戚的安利,非常喜歡Scala;但由于我們公司絕大部分開發都是基于Python,就連數據相關的Spark項目也是直接上PySpark,這個擰巴的語言就一直沒有被真正地推廣過。不過Hadoop不像Spark那樣對Python有原生API支持(支持力度大小暫且不論,但至少是親兒子級的API),而我又正好遇到了一個需求,要直接調用Hadoop API來查找文件,就只好被迫在JVM語言里選一個。由于Spark幾乎全是Scala寫的,學習一下對二次開發有幫助;Scala和Java相比又多了一個REPL,至少可以交互式地嘗試不熟悉的接口,于是我就又重新撿起了這個我并沒有真正掌握過的語言。寫了兩個很簡單的項目之后,在這里很簡單地回顧一下我和Scala的一些經歷,以及對Scala的一些想法——十分不嚴謹,也沒有太多原創的內容……但是我懶,如果想不起我的一些論點和論據是從哪些大牛的言論里引用來的時候,就先不注明了——不過如果有人回復了哪些人提過相同的想法,我會再另加注明的,走過路過的各位還請輕拍。
對于Scala,我和它的緣分還得從大三時候說起。那時的我平時是以寫Java為主的,但是因為大一時候編程入門課用的Scheme,對函數式編程有點初戀一樣的情節,時常YY著有一天能在JVM這個工程級別的平臺上寫函數式的語言。
有一天Coursera上開了門課,馬丁奧德司機大叔親自上去講課教大家用Scala,我就屁顛屁顛地入了坑,于是半個大三就成了白天在銀行實(神)習(游),晚上在宿舍參加社團活動,半夜紅著眼睛刷Scala作業的節奏,十分酸爽過癮萬分慶幸自己沒有猝死……于是終于成就了我在Coursera上拿下的第一門,也是至今唯一一門課……
老人們總說,技多不壓身,果然后來我去UCSB交換的時候,選的Programming Language這門課,上來教授就說,我們這課用Scala做實現——當時心里簡直樂開花,完全就是感覺自己努力得像一個傻逼一樣(其實并沒有)終于有回報了一般。但是殘酷的現實就是,Scala語言上的一丁點優勢,完全撐不過兩次作業,遇到課程大綱里真正的核心概念后就真是瞬間蕩然無存。于是乎,這門課只能吭哧吭哧地面向作業里的Test case編程,馬馬虎虎地混了個及格。
畢業之后,我懵懵懂懂地開始作一個網站開發,由于做的內容偏運營統計相關,而且日常工作以維護為主,對后端其實一直沒有形成很清楚的概念,就又開始YY著如果換一個語言或者平臺,開發的時候有強類型支持,我會不會學得快一點——現在回想還真是蠻蠢的哈——當時的我還嘗試過用Play來寫網站,然而從來沒有搞懂這一個巨獸級別的框架怎么上手。當然“巨獸”這個詞用出來會有J2EE、SSH的老鳥要笑我,不過乃們想想我現在還只是Flask菜鳥,連Django都覺得復雜,兩年前貿然挑戰Play也真是可笑啊。
不過在幾個月之后,用Flask從頭開始擼了一個微型小網站出來,也就放下學一個新框架的執念了。特別是最近一年多以來,憑著閑暇時候讀過的一些零零碎碎的博客,感覺自己也在慢慢反思什么語言才算得上稱手好用,也漸漸感覺到,Scala里過多的語法特性,為了追求和DSL接近的一些設計,確實是非常值得商榷的。
我最不習慣的一個語言特性就是Scala在語法上不區分無參調用和一個變量。舉一個簡單的例子:
scala> def a = { print("Hello world") }
scala> val b = a
上面兩行東西定義了一個函數a
和一個變量b
指向a
。現在來調用一下,看看會有什么結果:
scala> a
Hello World
scala> b
// WTF
可以看到,在調用a
的時候正確地輸出了我們要的信息,但是在調用b
的時候什么結果都沒有。當然,這樣的行為也并非所有人都不理解——例如習慣Ruby的筒子們應該非常清楚這是來自“面向對象”語言里的一種方法調用風格,叫做LISP-2,詳情可以參閱松本大叔的《代碼的未來》,簡單來說就是,上面的那句val b = a
,其實是把a
函數的執行結果給了b
,然后a
并沒有返回值,于是b
也就只返回了一個空值,咩都看不到~
喜歡Scala的人把這樣的設定說得十分美好,因為這樣的設定方便把代碼寫得像自然語言,畢竟自然語言里不那么歡迎括號;但是用JS和Python的人上手這樣的設定一般都很不習慣,因為這實在是有點含混不清的感覺——調用無參函數怎么就和一個變量長得一樣呢。。。那還怎么獲取對函數本身的引用?這里確實有一些方法可以達到類似的效果,但都和Scala的強類型設定多少有點沖突,我這里就不展開了,反正就是和括號玩游戲而已。對于非把編程語言寫得和自然語言接近,這在領域特定語言里好像是個蠻受歡迎的課題,但原諒我不認為這是一個好趨勢——畢竟自然語言是有很多歧義的,而編程語言里不應該有(這個觀點可以算是來自王垠吧,是他眾多觀點里我非常認同的一條)。
還有,對于強類型設定的沖突,我想再順便舉一個例子:
scala> val l = List(1, 2, 3, 4, 5)
l: List[Int] = List(1, 2, 3, 4, 5)
scala> l.map(_ + 2)
res5: List[Int] = List(3, 4, 5, 6, 7)
scala> def a(x: Int) = x + 2
a: (x: Int)Int
scala> l.map(a)
res6: List[Int] = List(3, 4, 5, 6, 7)
scala> l.map(a _)
res7: List[Int] = List(3, 4, 5, 6, 7)
scala> l.map(a(_))
res8: List[Int] = List(3, 4, 5, 6, 7)
我上面舉了四個例子的map
調用:第一個是最華麗也是最被提倡的,可惜不是所有調用都能這么簡單,于是我定義了一個函數來給所有的元素加2,同樣的列表直接map這個函數也是可以的;但是問題是,我如果寫了map(a(_))
,還是可以map,這就非常不直觀了——因為第一眼看上去,a(_)
是一個調用的結果——但是實際上,下劃線在Scala里有各種神奇的用法,這里就是其中之一,算是_ => a(_)
的簡寫形式。下劃線還不止于此,還能被用于類型匹配等其他方面——竊以為有點重載過度了,類型匹配里的下劃線用法比這個場景合理一些。
還有一些阻礙其他語言程序員上手的設定,不過有一些確實是不得已的權衡,不太像是一些設計上本來可以避免的、甜得過度的語法糖。合理權衡的例子比如集合類的逆變和協變,是出于類型安全的考慮;還有一個我同事吐了很久的槽(因為他之前寫Java),他不喜歡Scala代碼里的包聲明和路徑不對應,我感覺應該是為了支持一些動態腳本的特性(Scala Script)而放寬的限制吧——但是話說回來,一個類型安全的語言真的應該支持這些腳本特性嗎?或者其實沒準這不是一個繞不開的坑,只是實現上沒有做好?
總體上來說,Scala真的是一門非常非常神奇的語言,所有想得到想不到的特性都多少有些支持,單從這點來看,是非常獨到的。但是支持的特性又多到出現了不得已的沖突,讓整個語言看起來充滿了復雜的設計和甜到憂傷的語法糖。如果我們真的以后會有更大規模使用的話,可能得提前做好準備,做一份詳細的代碼風格要求,否則估計到時候會滿屏的縮寫和咒符吧括弧笑。
最后招個人吧……如果有志同道合的數據工程師或者增長黑客想來新加坡就業的話請發簡信 :)