計算機科學有兩大難題:快取失效,爲事物命名以及差一錯誤。
— 菲爾•卡爾頓
我們以程式語言中的物件,數值與函式形塑真實世界,雖然 Clojure 提供了列表、向量、映射與集合等基礎型態供我們使用,還是會有力有未逮的時候。
Clojure 除了基本型別之外,還提供了自行建立型別,以及擴充現有型態的功能,或許是因爲它知道,我們體內的物件導向火花尚未熄滅,仍然等待發光的時刻。
這次會介紹在 Clojure 當中如何自定型別,如何擴充已經存在的型別,還有如何使用更優雅的方式做到物件導向程式設計。
使用 defrecord
巨集可以建立自己的型別,第一個參數是型別的名字,接着在向量中分別寫上此型別內各個屬性的名稱:
(defrecord User [name age])
;; => user.User
defrecord
函式會根據名稱,動態地建立對應的 Java 類別,稱爲記錄類型 (Record),內部以類似映射的方式實作。你在參數向量中指定的屬性都可以公開取得,命名習慣則是跟 Java 中一樣,採取 CamelCase
的命名規則。以 defrecord
建立自定型別之後,以型別名稱後加上點 (.) 並接上各屬性的值,即可創建新型別:
(User. "Catherine" 40)
;; => #user.User{:name "Catherine", :age 40}
取得以 defrecord
建立的新型別中屬性的方法爲,在屬性名稱前加上點 (.),再帶入型別的執行個體 (Instance) 即可:
(.name (User. "Catherine" 40))
;; => "Catherine"
(.age (User. "Catherine" 40))
;; => 40
由於記錄類型實作了映射所屬的關聯類型 (Associative type),因此可以在映射上使用的函式,也可以套用在記錄類型上。
(assoc (User. "Catherine" 40) :city "Taipei")
;; => #user.User{:name "Catherine", :age 40, :city "Taipei"}
(dissoc (User. "Catherine" 40) :age)
;; => {:name "Catherine"}
以上的範例首先使用 assoc
爲記錄類型添加新的屬性,再使用 dissoc
將屬性之一刪除。由於在執行階段,動態地將原有類型的屬性刪除,因此返回值不再是記錄類型,而是映射。
記錄類型除了自動生成建構子函式(就是類別名稱加上點符號的函式)之外,還自動生成了工廠函式 (Factory function),用以建構記錄類型:
(->User "Catherine" 40)
;; => #user.User{:name "Catherine", :age 40}
另外還生成了可以將映射轉換成記錄類型的工廠函式,它接受一個映射,映射中包含了用於建構記錄類型的資訊:
(map->User {:name "Allen" :age 42})
;; => #user.User{:name "Allen", :age 42}
deftype
函式類似於 defrecord
,可以創建一個 Java 物件類型,並具有建構子函式:
(deftype Point [x y])
;; => user.Point
(.x (Point. 2 5))
;; => 2
(.y (Point. 2 5))
;; => 5
除此之外,便沒有與記錄類型相似的地方了。用 deftype
建立的類型既不能以映射的方式操作,也沒有工廠方法可供使用。
使用 defrecord
或是 deftype
創建具名的類別,如果臨時想要繼承某些類型,又因爲使用場所只在當前的環境,不需要大費周章創立具名類別,可以使用 reify
來建立匿名類型 (Anonymous type)。
reify
接受協定或是介面的名字爲參數,當作欲實作的類型,接下來是該類型中打算實作的方法,reify
可以同時繼承多個協定或是介面。
例如在 AWT/Swing 中,如果要接收來自各方的資訊,必須實作各種傾聽者 (Listener) 方法。可以使用 reify
來完成這個要求:
(reify
java.awt.event.MouseListener
(mousePressed [this e]
(println "Mouse pressed")))
以上範例實作了滑鼠傾聽者介面中的 mousePressed
方法。mousePressed
方法共有兩個參數,第一個參數爲發出此事件的執行實體,第二個參數則爲代表該滑鼠事件的執行實體。
Java 提供介面 (Interface) 用來定義共通的函式,由各型別實作共通的函式,根據各別的實作而有不同的功能。程式便只看見抽象的函式,而不依賴於實體類別。
在 Clojure 中可以使用 definterface
定義介面,與 Java 一樣:
(definterface IAnimal
(eat [food])
(sleep []))
;; => user.IAnimal
Clojure 還提供了類似介面的概念:協定。與介面類似,但是協定沒有實作部分,只有一組函式規則。介面一旦定義之後,便很難改動;協定則可以動態擴充,不必擔心牽一髮而動全身:
(defprotocol StackOps
(stack-push [this thing])
(stack-pop [this]))
;; => StackOps
以上範例使用 defprotocol
定義了一組堆疊的函式操作:可以往堆疊推入東西,也可以從堆疊中取出東西。
你可以使用 deftype
、defrecord
、reify
實作協定:
(deftype TypeStack [coll]
StackOps
(stack-push [_ thing] (println "Type push"))
(stack-pop [_] (println "Type pop")))
;; => user.TypeStack
(defrecord RecStack [coll]
StackOps
(stack-push [_ thing] (println "Record push"))
(stack-pop [_] (println "Record pop")))
;; => user.RecordStack
(reify StackOps
(stack-push [_ thing] (println "Reify push"))
(stack-pop [_] (println "Reify pop")))
;; => #object[user$eval10525$reify__10526 0x43ca678f "user$eval10525$reify__10526@43ca678f"]
(未完待續)