clojure - 在不同格式的 map 上调度函数调用

标签 clojure multimethod

我正在编写 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)慢,所以最终这是一个权衡。

这是完整的解决方案:https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj

关于clojure - 在不同格式的 map 上调度函数调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53329709/

相关文章:

clojure - 从 Clojure 中的文件打印和阅读列表

clojure - 从 Clojure 记录返回普通 map

julia - Julia 是如何实现多方法的?

java - 使用 Java 对象作为 Clojure 映射

clojure - clojure "and"和 "or"的非宏版本

clojure - clojure中断言为真时如何返回对应的数字

haskell - Clojure 中的协议(protocol)和多方法在多态性方面不如 Haskell 中的类型类强大的原因是什么?

Clojure多方法,如何添加数据?

clojure - defmethod 捕获所有

clojure - 多方法的通用语法