Home Resources

Clojure 多态(Polymorphism)

Table of Contents

defmulti & defmethod

Java 中,多态通过继承链达成,可以简单理解成,调用对象的方法时,使用对象的类型,沿着继承链 dispatch 到某个类上。

java-polymorphism.svg

在 Clojure 中,Multimethods 对 dispatch 这种模式进行抽象,defmulti 中定义的 dispatch 函数对于给定一组参数,生成一个用于 dispatch 的值,这个值被用于匹配定位特定一个 defmethod 实现,匹配使用 isa?.

由于 Class 的继承关系满足 isa? 关系,因此上面 Java 中的继承链多态可以简单模拟成:

java-inheritence-polymorphism-in-clojure.svg

另外 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

clojure-protocol-extending.svg

对于 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:

1

这段示例来自官方文档:https://clojure.org/reference/multimethods, 这里有对 Multimethods 比较详尽的解释.

Author: lotuc, Published at: 2023-04-23 Sun 00:00, Modified At: 2023-04-24 Mon 23:07 (orgmode - Publishing)