Clojure

多方法和层次结构

Clojure 避免了传统面向对象方法中为每种新情况创建新数据类型的做法,而是更倾向于在一小组类型上构建大量的函数库。但是,Clojure 完全认识到运行时多态性在实现灵活且可扩展的系统架构方面的价值。Clojure 通过一个多方法系统支持复杂运行时多态性,该系统支持根据一个或多个参数的类型、值、属性和元数据以及它们之间的关系进行分发。

Clojure 多方法是分发函数和一个或多个方法的组合。当使用defmulti定义多方法时,必须提供一个分发函数。此函数将应用于多方法的参数,以生成一个分发值。然后,多方法将尝试查找与分发值或派生分发值的关联方法。如果已定义了一个方法(通过defmethod),则将使用参数调用它,并且这将成为多方法调用的值。如果没有任何方法与分发值相关联,则多方法将查找与默认分发值(默认为:default)相关联的方法,如果存在则使用该方法。否则,调用将导致错误。

多方法系统公开了以下 API:defmulti 创建新的多方法,defmethod 创建并安装与分发值相关联的多方法的新方法,remove-method 删除与分发值相关联的方法,以及prefer-method 在方法可能存在歧义时创建方法之间的顺序。

派生由 Java 继承(对于类值)或使用 Clojure 的特定于领域的层次结构系统相结合来确定。层次结构系统支持名称(符号或关键字)之间的派生关系,以及类和名称之间的关系。derive 函数创建这些关系,而isa? 函数测试它们的存在。请注意,isa? 不是instance?

您可以使用 (derive child parent) 定义层次关系。Child 和 parent 可以是符号或关键字,并且必须是命名空间限定的。

注意 :: 读取器语法,::keywords 解析命名空间。

::rect
-> :user/rect

derive 是基本的关系创建者。

(derive ::rect ::shape)
(derive ::square ::rect)

parents / ancestors / descendantsisa? 允许您查询层次结构。

(parents ::rect)
-> #{:user/shape}

(ancestors ::square)
-> #{:user/rect :user/shape}

(descendants ::shape)
-> #{:user/rect :user/square}

(= x y) 意味着 (isa? x y)

(isa? 42 42)
-> true

isa? 使用层次结构系统。

(isa? ::square ::shape)
-> true

您还可以使用类作为子节点(但不能作为父节点,使某个节点成为类的子节点的唯一方法是通过 Java 继承)。

这允许您在现有的 Java 类层次结构上叠加新的分类法。

(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)

(isa? java.util.HashMap ::collection)
-> true

isa? 还会测试类关系。

(isa? String Object)
-> true

parents / ancestors 也是如此(但 descendants 则不是,因为类后代是一个开放集)。

(ancestors java.util.ArrayList)
-> #{java.lang.Cloneable java.lang.Object java.util.List
    java.util.Collection java.io.Serializable
    java.util.AbstractCollection
    java.util.RandomAccess java.util.AbstractList}

isa? 通过对其对应元素调用 isa? 来处理向量。

(isa? [::square ::rect] [::shape ::shape])
-> true

基于 isa? 的分发

多方法在测试分发值匹配时使用isa? 而不是 =。请注意,isa? 的第一个测试是 =,因此精确匹配有效。

(defmulti foo class)
(defmethod foo ::collection [c] :a-collection)
(defmethod foo String [s] :a-string)

(foo [])
:a-collection

(foo (java.util.HashMap.))
:a-collection

(foo "bar")
:a-string

prefer-method 用于在多个匹配且两者都不占主导地位的情况下消除歧义。您可以只声明,每个多方法,一个分发值优先于另一个。

(derive ::rect ::shape)

(defmulti bar (fn [x y] [x y]))
(defmethod bar [::rect ::shape] [x y] :rect-shape)
(defmethod bar [::shape ::rect] [x y] :shape-rect)

(bar ::rect ::rect)
-> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
   Multiple methods in multimethod 'bar' match dispatch value:
   [:user/rect :user/rect] -> [:user/shape :user/rect]
   and [:user/rect :user/shape], and neither is preferred

(prefer-method bar [::rect ::shape] [::shape ::rect])
(bar ::rect ::rect)
-> :rect-shape

以上所有示例都使用了多方法系统使用的全局层次结构,但是也可以使用make-hierarchy创建完全独立的层次结构,并且以上所有函数都可以将一个可选的层次结构作为第一个参数。

这个简单的系统非常强大。理解 Clojure 多方法和传统 Java 样式单分发之间关系的一种方法是,单分发就像一个 Clojure 多方法,其分发函数对第一个参数调用 getClass,并且其方法与这些类相关联。Clojure 多方法并非硬编码到类/类型,它们可以基于参数的任何属性、多个参数、执行参数验证并将路由到错误处理方法等。

注意:在此示例中,关键字 :Shape 用作分发函数,因为关键字是映射的函数,如数据结构部分所述。

(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
    (* (:wd r) (:ht r)))
(defmethod area :Circle [c]
    (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def r (rect 4 13))
(def c (circle 12))
(area r)
-> 52
(area c)
-> 452.3893421169302
(area {})
-> :oops