【问题标题】:How can I force dispatch to an existing multimethod implementation?如何强制调度到现有的多方法实现?
【发布时间】:2012-10-15 03:27:03
【问题描述】:

如果我为我的类型声明另一个命名空间(我无法更改的库)ns-a 的多方法:

defmethod ns-a/method-a Y [y]

在 ns-a 中定义了 X 的现有方法:

defmethod method-a X [x]

而调度函数是

(defmulti method-a type)

如果我的 Y 类型也是 X,我如何在 Y 的实现中分派给 X 的实现?

编辑:我发现了一个 hack:

(defmethod ns-a/method-a Y [y]
 (do-special-Y-stuff-here y)
    ; now do X stuff with Y:
    ((get-method ns-a/method-a X) y)
)

【问题讨论】:

  • 我知道 - 这就是我问的原因!我尝试了类型提示,但我想不出一种明确告诉它使用其他实现的方法。我想为我的类型做一些事情,然后委托给现有的方法来做剩下的事情
  • 但是说定义了defmulti的命名空间使用了类型,我不能改变它
  • 现在我想做更多...
  • 好的,但不是我想的那样。我刚刚做了 ns-a/method-a (create-an-X y)),其中 create-an-X 从我的 y 中创建了一个 X(即使我的 y 已经是 X 的一个实例,看起来很浪费但它有效)
  • no - dispatch 函数是给定的,它是由我无法更改的库定义的。我想扩展库的行为以支持我的类型 - 显然是多方法的经典用例

标签: types clojure multimethod


【解决方案1】:

如果您根据类型或类进行调度,请考虑修改您的代码以使用Protocols

更详细的答案:

如果还注册了精确的子类型,Clojure 的defmulti 不允许您分派到父 Java 类型。这是故意的(它在Liskov substitution principle 辩论中采取“最少惊喜”的一面)。由于 Y 已经注册了多方法,如果您的对象 isa? 恰好是 Y,那么您将点击 Y 的方法 - 即使它也是“X”。在 Java 中,类可以从多个接口继承,它们只能是完全一个类型。这就是你在这里遇到的问题。

根据documentation for multimethods

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

如果您查看 MultiFn 的源代码,您会发现 Clojure 总是使用给定的多方法调度值(在按类型调度时)最具体的 Java 类。

参见this line in Clojure 1.4.0 source for MultiFn,具体参见dominates的实现。

在 REPL:

user=> (defmulti foo #(class %))
user=> (defmethod foo java.util.RandomAccess [x] "RandomAccess")
user=> (defmethod foo java.util.Vector [x] "Vector")
user=> (defmethod foo java.util.Stack [x] "Stack")

好的,这按预期工作,prefer-method 不能覆盖类层次结构,因为isa? 首先检查 Java 类型。

user=> (prefer-method foo java.util.RandomAccess java.util.Stack)
user=> (foo (new java.util.Stack))
"Stack"

最后,在源代码中检查MultiFn 所有与调度值类型匹配的方法键(根据isa?)。如果找到多个匹配项,它会检查类型层次结构中的“支配”值。我们在这里看到Stack 支配RandomAccess

user=> (isa? java.util.Stack java.util.RandomAccess)
true
user=> (isa? java.util.RandomAccess java.util.Stack)
false

现在,如果我定义一个新方法 bar 如下:

user=> (defmulti bar #(class %))
user=> (defmethod bar Comparable [x] "Comparable")
user=> (defmethod bar java.io.Serializable [x] "Serializable")

由于模棱两可,我得到以下信息:

user=> (bar 1)
IllegalArgumentException Multiple methods in multimethod 'bar' match dispatch value: class java.lang.Long -> interface java.lang.Comparable and interface java.io.Serializable, and neither is preferred  clojure.lang.MultiFn.findAndCacheBestMethod (MultiFn.java:136)

现在,我可以用prefer-method解决这个问题

user=> (prefer-method bar Comparable java.io.Serializable)
user=> (bar 1)
"Comparable"

但是,如果我为Long 注册一个新方法

user=> (defmethod bar Long [x] "Long")
user=> (bar 1)
"Long"

我无法回到Comparable,即使我使用prefer-method

user=> (prefer-method bar Comparable Long)
user=> (bar 1)
"Long"

这似乎是你在这里遇到的。

请注意,您可以选择 remove-method - 但我认为与您设计的“黑客”相比,这是一个更重量级/更危险(猴子补丁?)的解决方案。

【讨论】:

  • 我不能,因为调度功能是由我无法更改的库提供的
  • 感谢您的全面回答!这可以作为多方法行为和期望的良好文档。关于prefer-method 的事情是,即使它可以用于将调度重定向到给定类型(抽象或具体),它也可能不是解决我的问题的好方法,因为prefer-method 似乎表示一个不确定的、全局的多方法行为的偏好,仅在本地和临时使用函数似乎不是一个好主意。
  • 将其标记为已接受的答案,因为它付出了努力!很好的解释了多方法。
【解决方案2】:

这应该可以使用prefer-method (doc):

用法:(prefer-method multifn dispatch-val-x dispatch-val-y)

原因 优先选择 dispatch-val-x 匹配的多方法 发生冲突时 dispatch-val-y

在你的情况下,你会说:

(prefer-method ns-a/method-a X Y)

这应该会导致在 ns-a 中为 dispatch-val X 定义的方法在 X 和 Y 的情况下被调用,并且在 Y 但不是 X 的情况下调用你的方法。

编辑: 原来prefer-method 是为了解决调度值不完全匹配的冲突,并且多个父级匹配,没有一个派生另一个。如果方法表中的调度值完全匹配,则始终使用该方法。所以这不会解决 OP 的用例。

【讨论】:

  • 这个要点没有冲突。 type 返回一个精确值:Long
  • ...看来prefer-method 是用于使用derive 构建的临时层次结构,而不是Java 类。
  • @noahz 在仔细检查 MultiFn 源后,问题是当调度值完全匹配时,prefer-method 无效。只有当 dispatch val 没有完全匹配,并且多个父级匹配且没有一个派生另一个时,它才会发挥作用。
  • 另外,prefer-method 确实适用于默认的 Java 类层次结构。见gist.github.com/3899779
【解决方案3】:

来到这里寻找相同问题的惯用解决方案。因为,根据@noahiz 的说法,习惯上不应该这样做,所以我将发布我们当前的解决方法,以供开发人员自行决定使用。只需通过defn 访问具体方法并将代理提供给defmulti

;;price/basic.cljs

(defn get-final-price-impl [{amount :amount}]
  amount) ;;normally this would be a complicated computation we want to keep DRY

(defmulti get-final-price :type)
(defmethod get-final-price :price/price-basic-tag [price-map]
  (get-final-price-impl price-map))
;;price/foreign-currency.cljs

(defmethod price-basic/get-final-price :price/price-foreign-currency-tag
  [price-map]
  (* (price-basic/get-final-price-impl price-map) (:conversion-rate price-map))) 

附:其中一个 cmets 建议重写类型并递归调用多方法。即,如果Y 派生自X,则在ns-a/method-a 中表示Y,我们应该使用(ns-a/method-a (create-an-X y))。不要这样做。如果 X 的 ns-a/method-a 本身调用其他多方法,则这些方法将被分派给 X 而不是 Y - 可能会出现模糊的逻辑错误。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-03-29
    • 2010-12-18
    • 1970-01-01
    • 2015-10-27
    • 2013-10-31
    • 1970-01-01
    • 2011-03-25
    • 1970-01-01
    相关资源
    最近更新 更多