使 Clojure 的 defprotocol 与现有函数一起玩得很好(多态)

chb*_*own 4 operator-overloading clojure clojurescript

我如何编写一个defprotocol(并defrecord实现它)来声明一个与现有函数同名的方法,并动态分派到协议/记录的方法,如果我用协议/记录的实例调用它,否则分派到现有的功能?

例如,我想创建一个支持基本算术的几何助手(在这个例子中只是乘法,以保持简短):

(defprotocol SizeOps
  (* [this factor] "Multiply each dimension by factor and return a new Size"))
Run Code Online (Sandbox Code Playgroud)

在这一点上,我已经从编译器那里得到了一些不祥的预感:

警告:协议#'user/SizeOps 正在覆盖函数 *
警告:* 已经引用:#'clojure.core/* 在命名空间中:user,被替换为:#'user/*

然后实现:

(defrecord Size [width height]
  SizeOps
  (* [this factor] (Size. (* width factor) (* height factor))))
Run Code Online (Sandbox Code Playgroud)

编译没问题,但是当我尝试使用它时,它唯一*知道的是我的协议中的那个:

(* (Size. 1 2) 10)
Run Code Online (Sandbox Code Playgroud)

IllegalArgumentException 没有方法的实现::* of protocol: #'user/SizeOps found for class: java.lang.Long

我可以通过*在我的实现中完全指定核心函数来解决这个问题:

(defrecord Size [width height]
  SizeOps
  (* [this factor] (Size. (clojure.core/* width factor) (clojure.core/* height factor))))
(* (Size. 1 2) 10)
Run Code Online (Sandbox Code Playgroud)

#user.Size{:width 10, :height 20}

IllegalArgumentException如果我稍后再打电话,我也会得到同样的结果(* 3 4)。我可以忍受使用命名空间clojure.core/*在我的defrecord实现,但我希望我的用户能够调用*Size,以及对记录LongDouble等等,像往常一样。

类似问答:

  • 5438379:延长String*:运营商的作品像Python (* "!" 3)"!!!",但*就像在我的例子中一样模糊了核心
  • 6492458:排除核心功能,例如(ns user (:refer-clojure :exclude [*]))避免“覆盖”警告,但也避免使用该功能:(
  • 1535235:相同,使用多方法但没有详细信息

我怀疑正确的解决方案存在于像defmultidefmethoddeftype/这样的低级调度功能中,derive但我对 Clojure 的运行时多态性的细微差别并不十分熟悉。而且我要去有一大堆SizePointRectangleCircle,等类型,每个支持的某个子集+-*/操作,所以我很想知道,如果有一种方法来告诉defprotocol参加/构建的多态性任何现有的功能,而不是简单地覆盖它们。

Sam*_*tep 5

在这种情况下,当您遇到协议本身的限制时,它可以帮助创建一个单独的函数,该函数仅调用其某些功能的协议方法,并使用附加的赋予常规defns 的能力:

(ns example.size
  (:refer-clojure :exclude [*])
  (:require [clojure.core :as clj]))

(defprotocol SizeOps
  (times [this factor]))

(extend-protocol SizeOps
  Object
  (times [this factor] (clj/* this factor)))

(defrecord Size [width height]
  SizeOps
  (times [this factor] (->Size (clj/* width factor) (clj/* height factor))))

(defn *
  ([] (clj/*))
  ([x] (clj/* x))
  ([x y] (times x y))
  ([x y & more] (apply clj/* x y more)))
Run Code Online (Sandbox Code Playgroud)

我在这里采用的特定方法有几个优点:

  • 除了双参数路径之外的所有路径都只使用 arity 调度(速度很快),而双参数路径仅另外使用协议调度(我认为它与您通常会获得的速度一样快)去做)
  • 你保留所有的参数,所以行为应该与clojure.core/*常规旧数字相同

随意根据需要优化其中的任何一个。

最后,证明:

(ns example.core
  (:refer-clojure :exclude [*])
  (:require [example.size :refer [* ->Size]]))

(* (->Size 1 2) 10) ;=> #example.size.Size{:width 10, :height 20}
(* 3 4) ;=> 12
Run Code Online (Sandbox Code Playgroud)

希望足够符合人体工程学,如前所述。