我正在编写 agar.io 克隆。我最近看到了很多限制记录使用的建议(例如 here ),因此我尝试仅使用基本 map 来完成整个项目。*
我最终为不同“类型”的细菌创建了构造函数,例如
(defn new-bacterium [starting-position]
{:mass 0,
:position starting-position})
(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))
“定向细菌”添加了一个新条目。 :direction
条目将用于记住它前进的方向。
问题来了:我想要一个一个函数take-turn
来接受细菌和当前的世界状态,并且返回一个 [x, y]
向量,指示细菌移动到的当前位置的偏移量。我想要一个被调用的函数,因为我现在可以想到至少三种我想要的细菌,并且希望能够在以后添加新类型每个人都定义自己的轮流
。
Can-Take-Turn
协议(protocol)已被排除在外,因为我只使用普通 map 。
take-turn
多方法一开始似乎可以工作,但后来我意识到我没有可在当前设置中使用的可扩展的调度值。我可以将 :direction
作为调度函数,然后在 nil
上调度以使用“定向细菌”的 take-turn
,或者默认情况下会获得基本的漫无目的的行为,但这并没有给我提供第三种“玩家细菌”类型的方法。
我能想到的唯一解决方案是要求所有细菌都有一个 :type
字段,并在其上进行调度,例如:
(defn new-bacterium [starting-position]
{:type :aimless
:mass 0,
:position starting-position})
(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :type :directed,
:direction starting-directions)))
(defmulti take-turn (fn [b _] (:type b)))
(defmethod take-turn :aimless [this world]
(println "Aimless turn!"))
(defmethod take-turn :directed [this world]
(println "Directed turn!"))
(take-turn (new-bacterium [0 0]) nil)
Aimless turn!
=> nil
(take-turn (new-directed-bacterium [0 0] nil) nil)
Directed turn!
=> nil
但现在我又回到了基本上按类型分派(dispatch),使用比协议(protocol)更慢的方法。这是使用记录和协议(protocol)的合法案例,还是我缺少有关多种方法的内容?我没有和他们进行太多练习。
*
我也决定尝试这个,因为我当时有一个 Bacterium
记录,并且想要创建一个新的“定向”版本的记录添加了一个字段direction
(基本上是继承)。虽然原始记录实现了协议(protocol),但我不想做一些事情,比如将原始记录嵌套在新记录中,并将所有行为路由到嵌套实例。每次创建新类型或更改协议(protocol)时,我都必须更改所有路由,这是一项繁重的工作。
最佳答案
您可以使用基于示例的多重调度来实现此目的,如 this blog post 中所述。 。它当然不是解决此问题的最高效的方法,但可以说比多种方法更灵活,因为它不需要您预先声明调度方法。因此它可以扩展到任何数据表示,甚至是 map 以外的其他东西。如果您需要性能,那么您建议的多种方法或协议(protocol)可能是正确的选择。
首先,您需要添加对 [bluebell/utils "1.5.0"]
的依赖项并 require [bluebell.utils.ebmd :as ebmd]
。然后,您声明数据结构的构造函数(从您的问题复制)和测试这些数据结构的函数:
(defn new-bacterium [starting-position]
{:mass 0
:position starting-position})
(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))
(defn bacterium? [x]
(and (map? x)
(contains? x :position)))
(defn directed-bacterium? [x]
(and (bacterium? x)
(contains? x :direction)))
现在我们将把这些数据结构注册为所谓的arg-specs,以便我们可以将它们用于调度:
(ebmd/def-arg-spec ::bacterium {:pred bacterium?
:pos [(new-bacterium [9 8])]
:neg [3 4]})
(ebmd/def-arg-spec ::directed-bacterium {:pred directed-bacterium?
:pos [(new-directed-bacterium [9 8] [3 4])]
:neg [(new-bacterium [3 4])]})
对于每个 arg-spec,我们需要在 :pos
键下声明一些示例值,并在 :neg 下声明一些非示例
键。这些值用于解决这样一个事实:定向细菌
比细菌
更具体,以便调度正常工作。
最后,我们将定义一个多态的 take-turn
函数。我们首先使用 declare-poly
声明它:
(ebmd/declare-poly take-turn)
然后,我们可以为特定参数提供不同的实现:
(ebmd/def-poly take-turn [::bacterium x
::ebmd/any-arg world]
:aimless)
(ebmd/def-poly take-turn [::directed-bacterium x
::ebmd/any-arg world]
:directed)
这里,::ebmd/any-arg
是一个与任何参数匹配的 arg-spec。上述方法与多种方法一样可以扩展,但不需要预先声明 :type
字段,因此更加灵活。但是,正如我所说,它也会比多方法和协议(protocol)慢,所以最终这是一个权衡。
关于clojure - 在不同格式的 map 上调度函数调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53329709/