當前位置:
首頁 > 最新 > 編程語言的動靜之爭:Clojure太靈活,我們該如何駕馭它?

編程語言的動靜之爭:Clojure太靈活,我們該如何駕馭它?

作者|何婧譽

編輯|小智

編程語言的聖戰,除了語言種類之分,也有動靜門派之別。我們寫著靜態語言往往想著動態語言的靈活,寫著動態語言又容易想著靜態語言的穩定和可靠。常聽到有人說,Clojure 確實優美,但動態語言實在駕馭不了,怎麼辦?

註:本文整理自 Morgan Stanley VP 何婧譽在 QCon 2017 北京站上的演講,原題為:《屬兔的處子——喜歡 Clojure,但怕動態語言太靈活怎麼辦》。

寫在前面

古話說的好,靜若處子,動若脫兔。這個我覺得非常適合形容動靜態語言的區別,靜態語言因為類型系統的關係,一直給人的是很穩定、很可靠,但是可靠到一定程度就變成了死板,會變成一個牢獄或者困住業務上所需的靈活性,因此常常需要很多層抽象,很多層膠水代碼,代碼就開始變得非常的晦澀,非常的難懂,而動態語言則完全是相反的。

常聽到有人說,Clojure 確實優美,但動態語言實在駕馭不了啊!沒有類型的幫助,在涉及到複雜的數據結構之後很容易失去對現有程序的理解,易讀性也會急速下降,而這也確實是 Clojure 作為動態語言所造成的問題。但是部分解決這個問題的辦法總是有的。core.typed 和 core.spec 兩個核心庫就可以幫助我們緩解動態語言太過野性框不住的問題,而本次演講的任務就是向大家介紹這兩個庫,以及這兩個庫解決這一問題的不同角度。

靜態語言 VS 動態語言

靜態語言因為類型系統的關係,一直給人的是很穩定、很可靠,但是可靠到一定程度就變成了死板,會變成一個牢獄或者困住業務上所需的靈活性,因此常常需要很多層抽象,很多層膠水代碼,代碼就開始變得非常的晦澀,非常的難懂。動態語言則完全是相反的,所有東西都是從類型上來講,以函數為例,靈活性已經足夠了,但是通常我們寫著寫著就忘記數據長什麼樣子了,你可能今天寫了一個函數說,輸入一個函數的數據,然後過了一個星期之後,我已經完全忘記這個數據是什麼東西了,因為生產環境裡面,類型系統在沒有編譯器的幫助下,基本上都是一次性的,這個問題對於用戶來說有相當大的困擾。

一直以來,這兩派之間沒有爭出特別的高低,靜態語言笑動態語言做不出大系統,動態語言笑靜態語言寫的太慢、廢話太多,今天這個主題當然不可能解決這個紛爭,但是希望通過 Clojure 這個語言可以給大家一些不太為人知的思路。馬上就有人來問了,我寫 Clojure 就是為了逃避這樣的內容來寫系統,這樣靈活多好用啊,我想寫什麼就寫什麼,快速原型靠的就是這個,我非常同意這一點。

簡單示例

在 Clojure 裡面有一個 json,因為動態語言的關係相當的簡單,完全沒有廢話。這個函數我覺得哪怕是不寫 Clojure 的,這個也是應該很能讀的懂的。首先有一個 Java 的 Reader,是 FileReader,這個 Reader 被傳遞到了這個 json 的函數裡面,讀出來文件內容,讀到 Map 裡面,但是讀完之後,你知道數據長什麼樣嗎?不知道,下次換一個 json 文件,同樣的函數可以同樣讀,但是你不知道讀出來是什麼東西。講到這裡就已經有一點難度在裡面了。

現在看一下,我現在讀完了要處理,我處理之後,我寫任意一個函數,如果說你不看這個函數寫的什麼東西,你知道它處理完成之後長什麼樣嗎?不知道,你知道他希望這個 json 數據是什麼樣的形狀嗎?不知道。我現在看了代碼之後,可以給你講,它裡面會有會有 Age、Name、Job、Address。

看一下 Age,它需要能夠使用 Int,那應該是個整數,但是要看代碼才知道,再下面還是簡單,那你們覺得 Name 的值是什麼東西?完全沒有使用到,它是一個 String,它是不是姓和名放在一起了?還是放在一個 Vector 裡面,可能姓和名是分開的,就是說不知道,要看代碼才知道。

你看到代碼之後覺得,原來是這樣,它應該是一個 Vector,或者是 List,姓和名是分開放,因為它這系,它用空格來 Join 一下,這個是一個很淺顯的例子,就已經說明了 Clojure 的動態靈活性非常強,但是也造成對數據的解釋性標記不是很清楚。

前文是一個很淺顯的例子,現在來看一個更具體的。為了這個主題我想了好幾天,覺得還是寫一個很小的項目來展示一下要講的東西。那寫什麼東西呢?我又想了好幾天,在此先謝謝鏈家。因為是這樣的,既然要來北京,就要關注一下房價。我到網上去看二手房信息,但一頁頁翻過去很累,我不可能手寫一個總結,於是就寫點程序抓取。當然這裡不是真的寫了一個爬蟲,只是抓幾個頁面做做樣子,沒有讓鏈家服務受到傷害,請鳥哥放心。

命名空間做的基本上就是通過一個庫把 html 讀進來之後,進行一些簡單的操作,把整理好的數據寫到一個 EDN 文件裡邊。比如說第一條你可以看到這個小區 1150 萬,三卧室兩個客廳,一個廚房兩個衛生間,包括面積之類的東西。再看這個數據轉換的函數,它收到一個參數是 Page,但這個 Page 長什麼樣完全不知道。我是通過庫讀進來的,讀進來之後,並不知道它長什麼樣子,現在看這個代碼也很難知道,它到底會返回一個什麼樣的類型,什麼樣的數據,如果將來需要擴展的話,或者將來我要給另外一個人用,或者幫助另外的一個人去做一些擴展,做一些維護很難搞定。

這就是前文說的 Clojure 作為一個動態語言的弊端——太靈活。這個弊端導致經常會忘記函數的參數是什麼樣子,而且這個是小項目,項目一大,那就更麻煩。可能有人會說的,文檔不就是做這個事情的嗎?文檔跟測試,沒有緊密的聯合在一起,文檔本身的代碼是剝離的,而相對代碼本身是沒有限制的。比如說很多代碼上面會寫,但是其實代碼裡面並沒有,它可能起到的效果某種程度上也是挺有限的。

Core.typed

Core.typed 是一個類型系統。它和其他語言的類型系統還是有點不一樣的地方,不同點在於它不是語言的一部分,而是一個即查即用的庫。Lisp 的靈活性導致它能夠作為一個庫直接插進去,而不是要作為一個語言核心。因為它有宏,通過宏可以把一個很大的類型系統直接插進去,而且這個類型系統比一般的系統要靈活很多,主要體現在這幾個方面:

第一,它可以給已經寫好的,沒有標註過的,或者說是用的庫裡面沒有標註過的函數直接加上類型;

第二,不需要把所有函數全部加上類型,你不想要的話,就不需要;

第三,你即使加上了也不一定要進行類型檢查,所以它是一個選擇性非常強的東西。它是為了能夠和 Clojure 這樣的語言進行協作。

那我們現在看一下它支持什麼東西:

OptionType,現在很流行,這個流行的語言現在都有這個結構。

Ordered Intersection Type 這個我不多講了,這個就是說一個函數,比如有兩種參數形式,這兩種參數類型可能又不一樣,你再進行類型檢查的時候,它會把這個參數從上到下有序的來進行一個匹配。unionType,寫過 Haskell 人都知道,這個很簡單,比如說整數,或者說是字元串,把它 union 一下,那就表示這個類型裡面的東西可以是字元串,也可以是函數。

Identity 是很簡單的函數,它會給你一模一樣的東西,那它的類型是什麼呢?它這個函數的類型是什麼東西呢?讓 Core.type 來幫我看一下。這個基本上可以看到前面有個 all,在這裡對所有的 X 能取得的類型它返回的是一個 X,就是 Polymorphism 最簡單的一個體現了。Occurrence Typing 這個東西見到的比較少,它是什麼呢?它是通過檢查代碼裡面寫的控制流,比如像 if,或者像 switch,它能夠進行類型推導。

舉個例子,首先把這個 Form 綁到 A 這個名字上面,值就是 1,但是我把它標註成了 any,就是說這個 A,就算只是 1,然後再返回 A,這裡大家覺得會返回什麼東西?如果是檢查一個類型,它最後返回的是 A,它是什麼類型?Any,因為我已經標過了,我說 A 是 Any,所以它就相信 A 是 Any,但是如果我這麼寫,這個會返回什麼東西?你可以看到它現在還是返回的是 A,這個 A 或者這個也是 A,那其他情況返回的是 Nil,那他現在覺得這個東西是什麼呢?還是不是 Any,因為你現在有了控制流在這邊,代碼里已經寫過了,所以它知道你只可能是 number,或者是 string,要不然就是 nil,所以最後 A 是 union string/number/nil。這個東西功能上是非常強大的,這個也是我強推的一個東西,這個你真正用起來就知道方便。

最後一個就是宏也會被展開之後再推導類型,宏跟大家剛剛知道的 switch 有點像,就是已經很直接了當的,告訴大家這個宏是可以展開之後判斷類型。我做的 Demo 的 types Demo,就是把剛剛鏈家那個小項目加了類型系統,我現在是把它所製造的結果定義類型,但它其實是什麼呢?是一個 Map。

Core.spec 總結

通過一個庫給動態語言加上類型系統——即插即用

可以給已經寫好的函數或者是用的無類型庫標註類型

可以選擇性地加上類型

加上了類型也並非一定要 type check

支持 Option Type,Ordered Intersection Types, Union Types

支持 Heterogenous Maps 和 Sequentials

支持 Polymorphism (All, Context Bounds),Higher-Kinds

支持 Occurrence Typing!(通過檢查 control flow 進行類型推導)

宏也會被展開後再推導類型

Core.spec

我本人很喜歡寫這個,我覺得給函數加上類型非常過癮,但是有問題,那有別的辦法嗎?有的,Core.spec,現在這個東西是 Clojure 核心,在很儘力地推廣。在方法上或者在函數上,加上先限條件,功能要強大一點,強大在什麼地方呢?

比方說生產環境,Runtime 不會受到影響,它的性能不會受到影響。因為如果你一天到晚在檢驗,它的性能上是會受到影響的,所以預設驗證是關閉掉的,如果你覺得某些東西可能重要性比較大,你要加上去也是可以的。spec 非常靈活,它可以把那種正則方式的 rule 給寫起來,就是比如某個 list,我覺得裡面開頭至少有一個字元串,然後後面跟著的至少是 0 個的整數等等,你就可以用正則裡面的加號,星號直接定義這個 rule。並且所有隻有一個參數的 predicate 的函數統統可以跟它進行無縫對接,不需要另外語法把它轉換成 spec。這裡面有很多種的驗證方式,那麼多的驗證的方式可能現在沒有時間講,就不講了,總體來說就是可以把數據套在一個很靈活的模子里。

Core.spec 總結

Runtime 性能基本不會受到影響(預設 spec 驗證關閉)

Map 的類型應該就是 key 及其對應的值的類型!(keys)

Sequence 可以多方面限制(cat, alt, regex style matching, coll-of)

只有一個參數的返回 boolean 值的函數通通都自動成為 predicate

各種驗證方式,滿足你的需求 (conform, explain, valid?)

multi-spec 支持更複雜的數據結構

Core.type vs Core.spec

寫在最後

core.typed 和 core.spec 你推薦哪個?

我的腦子喜歡 core.spec,因為有前景。我的內心喜歡 core.typed,因為給東西加類型寫起來真得很過癮。

寫《程序員修鍊之路》的 Andy Hunt 和 David Thomas 大師曾說,要在軟體開發這個行當立於不敗之地,應該「每年學一種新的語言」。10 月QCon 上海站上,C++ 之父 Bjarne Stroustrup會分享關於 C++ 語言的發展和未來編程語言格局,還有摩根大通高級程序員趙劼(老趙)、阿里中心主管楊冠寶(孤盡)、PingCAP 首席架構師唐劉、餓了么資深 Android 工程師張濤、滬江資深 Android 工程師何梁偉、Movoto 前端工程師吳名揚等,分享有關 Kotlin、Rust、TypeScript、.Net 的語言實踐,也歡迎你到現場和我們交流。


點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

閱讀架構師:來自矽谷/亞洲研究院的經驗與思考
如何開啟深度強化學習的大門?
直播進行中!全球運維技術大會智能化運維專題免費觀看!
程序員應該怎麼開啟器機學習之路呢?

TAG:InfoQ |