关于运算符重载:使Clojure的defprotocol与现有函数配合使用(多态)

Making Clojure's defprotocol play nice (polymorphically) with existing functions

我如何编写一个defprotocol(和defrecord来实现它)来声明一个与现有函数同名的方法,并动态分配给协议/记录的方法(如果我使用协议/实例的实例调用它的话)记录,但以其他方式调度到现有功能?

例如,我想创建一个支持基本算术的几何帮助器(在本示例中为乘法,以使其简短):

1
2
(defprotocol SizeOps
  (* [this factor]"Multiply each dimension by factor and return a new Size"))

在这一点上,我已经从编译器得到了一些不祥的推回:

Warning: protocol #'user/SizeOps is overwriting function *
WARNING: * already refers to: #'clojure.core/* in namespace: user, being replaced by: #'user/*

然后执行:

1
2
3
(defrecord Size [width height]
  SizeOps
  (* [this factor] (Size. (* width factor) (* height factor))))

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

1
(* (Size. 1 2) 10)

IllegalArgumentException No implementation of method: :* of protocol: #'user/SizeOps found for class: java.lang.Long

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

1
2
3
4
(defrecord Size [width height]
  SizeOps
  (* [this factor] (Size. (clojure.core/* width factor) (clojure.core/* height factor))))
(* (Size. 1 2) 10)

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

但是,如果稍后尝试调用(* 3 4),我将得到相同的IllegalArgumentException。我可以在defrecord实现中使用命名空间clojure.core/*,但是我希望我的用户能够在Size记录以及LongDouble等上调用*,照常。

类似的问答:

  • 5438379:使用类似于Python:(*"!" 3)*运算符扩展String吗? "!!!",但是像我的示例一样模糊了核心的*
  • 6492458:排除(ns user (:refer-clojure :exclude [*]))之类的核心功能可以避免出现"覆盖"警告,但也可以避免在:(
  • 1535235:相同,但有使用多种方法的手势,但没有详细信息

我怀疑正确的解决方案位于defmultidefmethoddeftype / derive之类的较低级别的调度功能中,但是我对Clojure运行时多态性的细微差别并不熟悉。我将拥有一整批SizePointRectangleCircle等类型,每种类型都支持+-*操作,所以我很想知道是否有一种方法可以告诉defprotocol参与/建立在任何现有函数的多态性上,而不是简单地覆盖它们。


在这种情况下,当您遇到协议本身的局限性时,可以帮助创建一个单独的函数,该函数简单地调用协议方法以实现其某些功能,并使用其他方法来完成其余的工作 常规defn的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(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)))

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

  • 除了两参数路径之外的所有路径都仅使用Arity调度(这是快速的),而两参数路径仅另外使用协议调度(我认为这与您通常会为尝试的结果而获得的速度一样快) 去做)
  • 您保留所有Arities,因此对于常规旧数字,其行为应与clojure.core/*相同

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

最后,演示:

1
2
3
4
5
6
(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

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