Clojure 多态(Polymorphism)
Table of Contents
defmulti & defmethod
Java 中,多态通过继承链达成,可以简单理解成,调用对象的方法时,使用对象的类型,沿着继承链 dispatch 到某个类上。
在 Clojure 中,Multimethods 对 dispatch 这种模式进行抽象,defmulti 中定义的 dispatch 函数对于给定一组参数,生成一个用于 dispatch 的值,这个值被用于匹配定位特定一个 defmethod 实现,匹配使用 isa?.
由于 Class 的继承关系满足 isa?
关系,因此上面 Java
中的继承链多态可以简单模拟成:
另外 Clojure 中可以用
derive
函数构建 symbol 或者 keyword 之间的
isa?
关系,你可以1:
(defmulti area :Shape)
(defmethod area :Rect [r] (* (:wd r) (:ht r)))
(defmethod area :Circle [c] (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(area {:Shape :Rect :wd 4 :ht 3}) ; => 12
(area {:Shape :Circle :radius 3}) ; => 28.274333882308138
(area {:Shape :SomeUnkownShape}) ; => :oops
protocols
对于 protocol 的说明,这里拷贝翻译一些官方文档2的内容。
protocol 由一组方法签名构成,可以通过
defprotocol
进行定义:
(defprotocol AProtocol
"A doc string for AProtocol abstraction"
(bar [a b] "bar docs")
(baz [a] [a b] [a b c] "baz docs"))
- 不用给定实现
- 可以可选的给定一个文档字符串
- 上面的定义会生成一组多态的函数(polymorphic function)和一个 protocol 对象
- 生成的这组函数将通过它们的第一个参数进行 dispatch,即所有方法至少有一个参数
我们可以在 defrecord, deftype 时直接实现 protocol,也能使用 reify 直接创建 protocol 的一个实例。
(defprotocol P (echo [x]))
(defrecord Foo [a]
P (echo [x] a))
(deftype Bar [a]
P (echo [x] a))
(echo (Foo. 42)) ; => 42
(echo (Bar. 42)) ; => 42
(echo (reify P (echo [x] 42))) ; => 42
也可以直接扩展某个已经定义(可能这个定义不受你控制)的类型实现特定 protocol.
(defprotocol P (bar [x]))
(defprotocol Q (baz [x]))
(defrecord Foo [a])
;; 扩展一个类型,为其实现多个 protocol
(extend Foo
P {:bar (fn [x] [:bar (:a x)])}
Q {:baz (fn [x] [:baz (:a x)])})
(bar (Foo. 42)) ; => [:bar 42]
(baz (Foo. 42)) ; => [:baz 42]
有两个展开到 extend
的 macro 方便 extend 的使用:
;; 这个简化了 extend 的书写
(extend-type Foo
P (bar [x] [:bar (:a x)])
Q (baz [x] [:baz (:a x)]))
;; 简化多个类型实现一个 protocol 的书写
(extend-protocol P
AType (bar [x] [:a-type (:a x)])
BType (bar [x] [:b-type (:a x)]))
Clojure 1.10 开始,支持通过 metadata 实现 protocol:
(defprotocol S
:extend-via-metadata true
(echo [s]))
(def a-dict {:name "42"})
(def a-dict-with-meta (with-meta a-dict {`echo (fn [s] (:name s))}))
(echo a-dict-with-meta) ; => 42
Footnotes:
这段示例来自官方文档:https://clojure.org/reference/multimethods, 这里有对 Multimethods 比较详尽的解释.