本文來自網易雲社區
李諾
" Clojure is elegant and pragmatic; it helps me focus more on solving business problems."
不同於Java這類靜態語言, Clojure是動態語言,動態類型意味著這些類型會在代碼運行時由Clojure動態推匯出來,編譯時間不作任何限制。
user=> (defn f1 [a b] (+ "1" 2))#'user/f1
user=> (f1 1 2)ClassCastException java.lang.String cannot be cast to java.lang.Number clojure.lang.Numbers.add (Numbers.java:128)
對比上面的兩端代碼,即使函數f1中含有類型有誤的運算式(+ "1" 2)也是可以定義的。然而一旦啟動並執行時候(我們隨意給了兩個沒有用到的參數),函數+會嘗試將"1"和2投射(cast)到Java中的Number類,這時候代碼中的類型錯誤就會以ClassCastException拋出來,因為字串"1"是無法被投射到Number類的。
注意: 投射(cast)和轉換(convert)並不一樣,cast一般是將一個子類的成員投射到它的超類上,而像Javascript中"1" + 2 = 3所做的就是隱式轉換了。Clojure的原生函數沒有使用隱式轉換。
所以我們在寫代碼的時候一般不需要給定資料類型,但這不意味著類型就不重要了。
為了更好地認識Clojure的類型,首先要會用一個函數: type
user=> (doc type)-------------------------clojure.core/type([x]) Returns the :type metadata of x, or its Class if nonenil
我們可以用這個函數擷取定義中的中繼資料中的:type項,如果沒有這項,那麼就返回它的類名。
基礎資料型別 (Elementary Data Type)
我們先來看一些常見的值被推導的預設類型:
數值
user=> (type 1)java.lang.Long
Clojure的整數預設使用Java中的Long基礎類型,
user=> (type Math/PI)java.lang.Double
小數使用Java中的Double類型。
如果我們想要使用其他一些java中的基本類型的整數或者浮點數,我們可以使用
user=> (type (int 1))java.lang.Integeruser=> (type (float 3.14))java.lang.Float
這類強制轉換(Coerce)函數來產生。
有興趣的同學可以去試試 (type 1111111111111111111111111111111111111111) 和 (type (/ 1 2)) 是什麼類型的。
字串
user=> (type \a)java.lang.Characteruser=> (type "Hello")java.lang.String
字元和字串同樣也是來自Java。
關鍵詞
可以看到,數字,字串都使用的是Java的類型,而Clojure中也有一些專屬的內建基礎類型,如關鍵詞,
user=> (type :hello)clojure.lang.Keyword
如類似:a,:book,:code這樣的關鍵詞。關鍵詞的相等性測試非常快,所以關鍵詞常見的使用情境是map類型(之後會詳細介紹)中的key。
在下面的這個benchmark中,我們對比了從兩種map中擷取key對應的value的調用時間,使用關鍵詞做key的map比另一個使用字串做key的map快了50%,
insight.main=> (def t {:hello 1}):hella#'insight.main/tinsight.main=> (quick-bench (get t :hello))WARNING: Final GC required 76.32000953339649 % of runtimeEvaluation count : 55186818 in 6 samples of 9197803 calls. Execution time mean : 10.005364 ns Execution time std-deviation : 1.900011 ns Execution time lower quantile : 8.087005 ns ( 2.5%) Execution time upper quantile : 12.200609 ns (97.5%) Overhead used : 2.154486 nsnilinsight.main=> (def t1 {"hello" 1})#'insight.main/t1insight.main=> (quick-bench (get t1 "hello"))WARNING: Final GC required 71.39921475186507 % of runtimeEvaluation count : 39262944 in 6 samples of 6543824 calls. Execution time mean : 15.356626 ns Execution time std-deviation : 0.589740 ns Execution time lower quantile : 14.530609 ns ( 2.5%) Execution time upper quantile : 16.016271 ns (97.5%) Overhead used : 2.154486 nsFound 1 outliers in 6 samples (16.6667 %) low-severe 1 (16.6667 %) Variance from outliers : 13.8889 % Variance is moderately inflated by outliersnilinsight.main=>集合(Collections)
要看懂Clojure的代碼,只有前面的這些類型還不夠,還需要一些基本的資料結構,如Vector,List,Map,Set等等,因為Clojure的代碼即資料。
List
List的是函數式語言裡常見的一種資料結構,我們可以有兩種基本的方法構成
user=> (type '(1 2 3))clojure.lang.PersistentListuser=> (type (list 1 2 3))clojure.lang.PersistentList
事實上,list的顯示是不需要'(quote)或者list的
user=> (map inc '(1 2 3))(2 3 4)
那為什麼在構建list的時候不能這樣寫呢?其實原因很簡單,
list是構成Clojure代碼的重要組成部分!
回看Clojure這類Lisp語言的文法,到處可見如(type 1),(+ 1 2)這樣的list形式的代碼,其實形如
(operator operand1 operand2 ... operandn)
這樣的代碼中,第一項operator會被當做可執行檔函數, 後面的n項operands則是operator的參數。簡單的來說,就是以list的形式寫的運算式,預設會被當成一個函數應用(function application)的運算式求值(evaluate), 而',也就是quote函數,能阻止求值。
求值:
user=> (list 1 (+ 1 2) 3)(1 3 3)
不求值:
user=> '(1 (+ 1 2) 3)(1 (+ 1 2) 3)
Clojure語言具有同像性(homoiconicity),也就是說,代碼一般是以資料的形式組織在一起的,這讓作業碼變得和操作資料一樣容易,方便了元編程(Clojure中可以編寫宏)。
回到list的類型上,list在Clojure裡面被叫做 PersistentList 也就是"持久化的list",是不可變資料類型的一種。Clojure的持久化的資料結構方便了在資料不可變的前提下,對於基於已有資料結構的“修改”操作(資料不可變,“修改”本質上來說是新資料的建立),能夠儘可能的結構共用(structure sharing)。
持久化list的結構共用可以用下面的這個例子解釋(來自Wiki):
(def xs '(0 1 2))(def ys '(3 4 5))
將這兩個list做連接合并(concat)
(def zs (concat xs ys))
可以看到xs和ys都存留了下來,在記憶體中只有xs做了複製,而ys被共用了,因為圖中藍色的部分和xs並不完全一樣
Vector
Vector是Clojure裡一般被理解作類似Java裡的ArrayList的資料結構。
user=> (type [0 1 2])clojure.lang.PersistentVector
Vector同樣也是持久化的資料類型。
事實上Vector是以樹形結構儲存的數組,它每一層最多有32個子節點,所以它的隨機訪問是O(log32n)的(也就是O(logn))。有興趣的朋友可以看Understanding Clojure's Persistent Vectors, pt. 1裡關於如何增加,刪減元素的非常詳盡的介紹。
類似地,我們也可以用
user=> (vector 1 2 3)[1 2 3]
來組成Vector,或是用
user=> (vec '(1 2 3))[1 2 3]
將list或其他結構轉換成vector。
我們一般也可以把vector視作和array類似的,添加,更新,查詢都只要O(1)的結構。
Vector也是一個經常用於組織代碼,如在函數定義中
(defn plus [a b] (+ a b))
函數名字和函數體之間的函數參數就是以vector的形式給出的。其他的還有let中
(defn f1 [a b] (let [sum (+ a b) diff (- a b)] (* sum diff)))
我們需要給出一個有偶數對成員的且滿足一些特殊條件的vector。
Set
在Clojure中,Set也是內建的資料結構,有特別的書寫形式
user=> (type #{1 2 3})clojure.lang.PersistentHashSet
Set的成員具有唯一性,我們可以直接把它當做一個函數使用,判斷一個值是否是集合中的一員
user=> (def s1 #{1 3 5 7})#'user/s1user=> (s1 3)3user=> (s1 4)nil
當這個成員存在於集合中,會返回它自身,不然則返回nil(等同於null)。
Map
Map是Clojure中使用頻率非常高的資料結構,對於簡單的map,我們可以直接在{``}中寫偶數個成員, key和value均不用使用同一類型,
user=> (type {:name "doge" "age" 2})clojure.lang.PersistentArrayMap
預設的map類型為ArrayMap, 它的結構和Vector比較類似,裡面的條目是按照建立的順序排列的,如果要建立HashMap,我們可以用
user=> (hash-map :name "X" :age "10"){:age "10", :name "X"}user=> (type (hash-map :name "X" :age "10"))clojure.lang.PersistentHashMap
Map同樣可以被當做函數執行,
user=> (def m1 {:name "X" :age "10"})#'user/m1user=> (m1 :name)"X"user=> (m1 :title)niluser=> (m1 :title "default value")"default value"
會在map中尋找第一個給入的參數作為key的value,如果沒有,預設返回nil,如果有第二個參數,會代替nil返回。
Clojure給Map的操作提供了不少便捷的函數,我們可以用get來擷取某個key對應的value
user=> (def m1 {:age 10 :name {:firstname "John" :surname "Smith"}})#'user/m1user=> (get m1 :name){:firstname "John", :surname "Smith"}
而當我們需要查詢更深層的value時候,我們可以使用get-in,搭配上一個按順序給出的keys的vector
user=> (get-in m1 [:name :surname])"Smith"
當我們要插入或是覆蓋某個key對應的value時,我們可以使用assoc
user=> (assoc m1 :height 150){:age 10, :name {:firstname "John", :surname "Smith"}, :height 150}
如果需要更新某個value,可以使用update
user=> (update m1 :age (fn [x] (+ x 1))){:age 11, :name {:firstname "John", :surname "Smith"}}
update和assoc的區別在於update可以把原來的值作為產生新的值的一個參數,其實他也等同於你用get之後再用assoc,用下面的等式來表達
(update m k f) = (assoc m k (f (get m k)))
在使用的時候,使用最貼合代碼邏輯的寫法,會更有利於代碼的可讀性和可維護性。後面的寫法如果要等於左邊m和k都出現了兩次,潛在的增加了修改時的風險。但是後面的寫法有時會顯得更為靈活。
和get類似,Clojure也提供了assoc-in和update-in,配合上一個常用的宏->使用,可以讓map的操作寫起來非常簡潔直觀
user=> (-> {:name {:fullname "Xiao Wang"} :from {:Country "China" :City "Beijing"}} #_=> (assoc :age 30) #_=> (assoc-in [:from :City] "Shanghai") #_=> (update-in [:name :fullname] (fn [full-name] (clojure.string/split full-name #" "))) #_=> ){:name {:fullname ["Xiao" "Wang"]}, :from {:Country "China", :City "Shanghai"}, :age 30}總結
認識了這些基本的資料類型之後,Clojure的代碼將會變得非常容易上手,歡迎感興趣的朋友們嘗試Clojure,Clojure的簡潔和直觀一定能夠讓你印象深刻,讓你能夠更focus在解決真正的問題上。安裝的方法可以參考之前我寫的這篇,
答案
user=> (type 111111111111111111111111111111111111111111111111111)clojure.lang.BigIntuser=> (type (/ 1 2))clojure.lang.Ratio
網易雲新使用者大禮包:www.163yun.com/gift
本文來自網易雲社區,經作者李諾授權發布。
相關文章:
【推薦】 一份ECMAScript2015的代碼規範(上)