Clojure 如何使记录转发协议?
我想为记录的每个实例附加不同的协议。在Clojure中,什么是干净、不重复的方法 具体来说,我有一个类似这样的协议:Clojure 如何使记录转发协议?,clojure,delegates,polymorphism,protocols,record,Clojure,Delegates,Polymorphism,Protocols,Record,我想为记录的每个实例附加不同的协议。在Clojure中,什么是干净、不重复的方法 具体来说,我有一个类似这样的协议: (defprotocol LinkPolicy (lp-boost [dock g to]) (lp-reduce-to-uncommitted [dock g to]) (lp-reciprocate-commitment [dock g from]) (lp-reciprocate-no-commitment [dock g from]) (lp-norm
(defprotocol LinkPolicy
(lp-boost [dock g to])
(lp-reduce-to-uncommitted [dock g to])
(lp-reciprocate-commitment [dock g from])
(lp-reciprocate-no-commitment [dock g from])
(lp-normalize-after-add [dock g to weight])
(lp-committed? [dock g to])
. . .) ; more methods than this, even
(defrecord Dock [nodeid name link-policy]
LinkPolicy
(forward all methods to link-policy))
(defrecord Dock [nodeid name link-policy]
LinkPolicy
(lp-boost [dock g to]
(lp-boost link-policy dock g to))
(lp-reduce-to-uncommitted [dock g from]
(lp-reduce-to-uncommitted link-policy dock g from))
(lp-reciprocate-commitment [dock g from]
(lp-reciprocate-commitment link-policy dock g from))
; lots more forwarding methods here . . .
. . .)))
我想定义一个类似这样的记录:
(defprotocol LinkPolicy
(lp-boost [dock g to])
(lp-reduce-to-uncommitted [dock g to])
(lp-reciprocate-commitment [dock g from])
(lp-reciprocate-no-commitment [dock g from])
(lp-normalize-after-add [dock g to weight])
(lp-committed? [dock g to])
. . .) ; more methods than this, even
(defrecord Dock [nodeid name link-policy]
LinkPolicy
(forward all methods to link-policy))
(defrecord Dock [nodeid name link-policy]
LinkPolicy
(lp-boost [dock g to]
(lp-boost link-policy dock g to))
(lp-reduce-to-uncommitted [dock g from]
(lp-reduce-to-uncommitted link-policy dock g from))
(lp-reciprocate-commitment [dock g from]
(lp-reciprocate-commitment link-policy dock g from))
; lots more forwarding methods here . . .
. . .)))
我想我可以实现如下转发:
(defprotocol LinkPolicy
(lp-boost [dock g to])
(lp-reduce-to-uncommitted [dock g to])
(lp-reciprocate-commitment [dock g from])
(lp-reciprocate-no-commitment [dock g from])
(lp-normalize-after-add [dock g to weight])
(lp-committed? [dock g to])
. . .) ; more methods than this, even
(defrecord Dock [nodeid name link-policy]
LinkPolicy
(forward all methods to link-policy))
(defrecord Dock [nodeid name link-policy]
LinkPolicy
(lp-boost [dock g to]
(lp-boost link-policy dock g to))
(lp-reduce-to-uncommitted [dock g from]
(lp-reduce-to-uncommitted link-policy dock g from))
(lp-reciprocate-commitment [dock g from]
(lp-reciprocate-commitment link-policy dock g from))
; lots more forwarding methods here . . .
. . .)))
但这似乎没有我在Clojure所期望的那么优雅。而且,每次我重新定义LinkPolicy时,我也必须修改Dock。(Dock中的方法和链接策略
方法之间没有名称冲突吗?)
有什么更好的方法?更新
我偶然发现一个宏,它允许定义一个协议,将所有方法调用委派给库中的另一个对象:。它位于实验名称空间下,但您可能会发现它。。。有用:)
这不是你的答案的直接答案,但我想我会分享一些想法,你可以如何处理你的问题 我不确定LinkPolicy协议的实现是什么样子的,但从包含的代码片段来看,该协议似乎包含很多方法。这可能是违反界面分离原则的迹象。另外,我不知道您的实现之间是否/如何不同-您是否有一些实现对于某些协议方法具有完全相同的实现 如果是这样的话,我会考虑使用多种方法。这将使您能够灵活地为每个方法使用单独的分派逻辑,并将各种对象分派到同一个实现 您可以为每个协议方法定义单独的multimethod:
(defmulti lp-boost
;; dispatch function
(fn [g to]
(cond
(some-cond-1 ...) :dispatch-value-1))
(defmulti lp-reduce-to-uncommitted
(fn [g to] ...))
;; and so on for remaining functions
您的分派函数可以返回一个简单的关键字,甚至是一个向量或多个关键字,以便您可以分派,例如使用对象类型和一些其他属性(例如,对于Dock
,您可以拥有[:Dock:lp-boost-impl-1]
或[:Dock:lp-boost-impl-2]
等。)
然后,您可以为分派值定义实现:
(defmethod lp-boost :dispatch-value-1
[g to]
...)
使用multimethods不会有协议提供的一些好的属性(所有的行为都是分组的,如果一个类型实现了一个协议,那么所有的方法都将为它定义,更好的分派性能等等),但是如果你真的需要,你将在选择方法实现方面获得很大的灵活性。(我真的很怀疑),有一种方法:
假设我们有一个协议StuffDoer
:
(defprotocol StuffDoer
(do-important-stuff [this x])
(do-other-important-stuff [this x y]))
事实上,该协议只是定义协议行为的映射:
user> StuffDoer
{:on user.StuffDoer, :on-interface user.StuffDoer,
:sigs {:do-important-stuff {:name do-important-stuff, :arglists ([this x]), :doc nil}, :do-other-important-stuff {:name do-other-important-stuff, :arglists ([this x y]), :doc nil}},
:var #'user/StuffDoer, :method-map {:do-important-stuff :do-important-stuff, :do-other-important-stuff :do-other-important-stuff},
:method-builders {#'user/do-other-important-stuff #function[user/eval20549/fn--20550],
#'user/do-important-stuff #function[user/eval20549/fn--20565]}}
所以我们有所有方法的定义,在extend
中使用它们。
让我们将此自定义扩展函数:
(defn extend-forwarding [t p fwd-to]
(extend
t p
(into {} (map (fn [[method-var _]]
[(keyword (.sym method-var))
(fn [this & args]
(apply method-var (fwd-to this) args))])
(:method-builders p)))))
它动态地为extend
构建一个映射,获取协议的所有方法,并为每个方法生成代理函数
让我们测试一下:
(defrecord ConcreteDoer [v]
StuffDoer
(do-important-stuff [this x]
(println "doing stuff:" v x))
(do-other-important-stuff [this x y]
(println "doing other stuff:" v x y)))
(defrecord ConcreteDoerWithForwarding [fwd-inst])
答复:
user> (extend-forwarding ConcreteDoerWithForwarding StuffDoer :fwd-inst)
nil
user> (do-important-stuff
(ConcreteDoerWithForwarding. (ConcreteDoer. :aaa))
10)
doing stuff: :aaa 10
nil
user> (do-other-important-stuff
(ConcreteDoerWithForwarding. (ConcreteDoer. :aaa))
10 20)
doing other stuff: :aaa 10 20
nil
自动定义记录类型,每个协议一个
诀窍是以编程方式定义一个单独的记录类型,对应于每个不同的协议。也就是说,每当程序出现一个您想要“粘贴”的不同方法映射时,它都会定义一个新的记录在原始记录类型上。显然,只有当不同协议的数量足够小,以至于不会用这些不同的人工记录类型占用内存时,这才有效。此外,您不能使用此技巧将不同的链接协议分配给不同的关键字(见下文)
下面的函数为给定的“dockclass”定义一个记录类型,它是一个包含链接协议的名称和方法映射的映射。它返回构造函数
(def ^:dynamic link-policy-for-eval)
(defn make-dock-maker [dockclass]
(binding [link-policy-for-eval (:link-policy dockclass)]
(let [dockclass-name (name (:name dockclass))
record-sym (symbol (str "Dock-" dockclass-name))
ctor-sym (symbol (str "->Dock-" dockclass-name))]
(eval `(defrecord ~record-sym ~'[nodeid dock-name]))
(eval `(extend ~record-sym
~'fargish.links/LinkPolicy
link-policy-for-eval))
ctor-sym))))
例如,如果您有一个名为:input
的dockclass,make dock maker
将定义一个名为dock input
的记录,并返回其构造函数->dock input
我将该构造函数存储在一个可以被一个名为dock
的函数检索的位置,该函数调用它来构造一个新的dock:
(defn dock [node dock-name]
(let [maker (get-in node [:dock dock-name :dockclass :maker])]
(maker (:nodeid node) dock-name)))
因此,只需调用一个函数,就可以使用适当的链接协议创建适合节点的dock。代码中没有任何地方需要按名称引用任何生成的记录类型或构造函数
有一件主要的不明显的事情需要注意,这让我花了很多精力去弄清楚:
- 生成的代码,如eval或宏中的代码。var
eval的链接策略
提供了一个存放方法映射(其值均为函数)的位置,eval可以在该位置查看它
为什么多态转发这么好
当被链接的对象满足LinkPolicy,而不是提供获取LinkPolicy的方法时,调用代码非常简单。例如,下面的一些代码利用了具有不同链路协议的码头上的自动调度:
(defn make-reciprocal-edits-for [g {:keys [from to]}]
(if (lp-committed? from g to)
(lp-reciprocate-commitment to g from)
(lp-reciprocate-no-commitment to g from))))
如果我在使用链接策略之前必须从每个dock提取链接策略,那么代码不会变得非常复杂,但它肯定会在所有地方添加一些混乱,并模糊了主要思想
为了编写一个简单的单元测试,我想使用dock的关键字,而不是在整个程序运行时构建引用所有所需内容的记录。extend
简化了这一过程:
(deftest test-exclusive-linking
(extend clojure.lang.Keyword LinkPolicy (merge vanilla exclusive-linking))
(->/do (uber/digraph :a :b :c)
(do-action (Boost. :a :b))
(->is (= <> (uber/digraph :c [:a :b 1.0])))
(do-action (Boost. :a :c))
(->is (= <> (uber/digraph :b [:a :c 1.0])))))
(deftest独占链接
(扩展clojure.lang.Keyword链接策略(合并独占链接))
(->/do(优步/有向图:a:b:c)
(执行操作(增压:a:b))
(->is(((优步/有向图:c[:a:b1.0]))
(执行操作(增压:a:c))
(->是(((优步/有向图:b[:a:c1.0])))))
只要每个关键字都有相同的链接策略就可以了。如果你想给不同的关键字提供不同的链接策略,那么你必须使用一种更通用的方法,如其他答案所示。所以“记录的每个实例”是指每个扩展都有多个不同类型的记录