2

我正在写一个 agar.io 克隆。我最近看到了很多限制使用记录的建议(比如这里),所以我试图只使用基本地图来完成整个项目。*

我最终为不同“类型”的细菌创建了构造函数,比如

(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]指示从当前位置移动细菌到的偏移量。我想要一个被调用的函数,因为我现在可以想到至少三种我想要拥有的细菌,并且希望以后能够添加新的类型,每种类型都定义了自己的take-turn.

Can-Take-Turn协议不在窗口,因为我只是使用普通地图。

多方法一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

但现在我又回到了基本的类型调度,使用比协议更慢的方法。这是使用记录和协议的合法案例,还是我缺少关于多种方法的东西?我和他们没有太多的练习。


*我也决定尝试这个,因为我有一个Bacterium记录并且想要创建一个新的“定向”版本的记录,其中direction添加了一个字段(基本上是继承)。虽然原始记录实现了协议,但我不想做一些事情,比如将原始记录嵌套在新记录中,并将所有行为路由到嵌套实例。每次创建新类型或更改协议时,我都必须更改所有路由,这是很多工作。

4

3 回答 3

2

您可以为此使用基于示例的多次调度,如本博文中所述。这当然不是解决这个问题的最高效的方法,但可以说比多方法更灵活,因为它不需要你预先声明一个调度方法。因此,它可以扩展到任何数据表示,甚至是地图以外的其他事物。如果您需要性能,那么您建议的多方法或协议可能是要走的路。

首先,您需要添加对[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。这些值用于解决 a比adirected-bacterium具体bacterium的事实,以便调度正常工作。

最后,我们将定义一个多态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)

在这里, the::ebmd/any-arg是一个匹配任何参数的 arg-spec。上述方法与多方法一样可以扩展,但不需要您:type预先声明字段,因此更加灵活。但是,正如我所说,它也会比多方法和协议都慢,所以最终这是一个权衡。

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

于 2018-11-16T09:06:07.307 回答
2

通过:type字段调度多方法确实是可以通过协议完成的多态调度,但是使用多方法可以让您在不同的字段上进行调度。您可以添加第二个多方法,该方法在 以外的其他内容上进行调度:type,这可能很难通过协议(甚至多个协议)来完成。

由于多方法可以调度任何东西,因此您可以使用集合作为调度值。这是另一种方法。它不是完全可扩展的,因为要选择的键是在调度函数中确定的,但它可能会给你一个更好的解决方案的想法:

(defmulti take-turn (fn [b _] (clojure.set/intersection #{:direction} (set (keys b)))))

(defmethod take-turn #{} [this world]
  (println "Aimless turn!"))

(defmethod take-turn #{:direction} [this world]
  (println "Directed turn!"))
于 2018-11-16T12:13:18.883 回答
1

快速路径的存在是有原因的,但 Clojure 不会阻止您做任何您想做的事情,例如,包括临时谓词调度。世界绝对是你的牡蛎。观察下面这个超级快速和肮脏的例子。

首先,我们将从一个原子开始来存储我们所有的多态函数:

(def polies (atom {}))

在使用中,内部结构polies看起来像这样:

{foo ; <- function name
 {:dispatch [[pred0 fn0 1 ()] ; <- if (pred0 args) do (fn0 args)
             [pred1 fn1 1 ()]
             [pred2 fn2 2 '&]]
  :prefer {:this-pred #{:that-pred :other-pred}}}
 bar
 {:dispatch [[pred0 fn0 1 ()]
             [pred1 fn1 3 ()]]
  :prefer {:some-pred #{:any-pred}}}}

现在,让我们这样做,以便我们可以使用prefer谓词(如prefer-method):

(defn- get-parent [pfn x] (->> (parents x) (filter pfn) first))

(defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2]
  (if-let [p (-> @polies (get-in [poly :prefer v1]))]
    (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1))))

(defn- default-sort [v1 v2]
  (if (= v1 :poly/default)
    1
    (if (= v2 :poly/default)
      -1
      0)))

(defn- pref [poly v1 v2]
  (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2)))
    -1
    (default-sort v1 v2)))

(defn- sort-disp [poly]
  (swap! polies update-in [poly :dispatch] #(->> % (sort-by first (partial pref poly)) vec)))

(defn prefer [poly v1 v2]
  (swap! polies update-in [poly :prefer v1] #(-> % (or #{}) (conj v2)))
  (sort-disp poly)
  nil)

现在,让我们创建我们的调度查找系统:

(defn- get-disp [poly filter-fn]
  (-> @polies (get-in [poly :dispatch]) (->> (filter filter-fn)) first))

(defn- pred->disp [poly pred]
  (get-disp poly #(-> % first (= pred))))

(defn- pred->poly-fn [poly pred]
  (-> poly (pred->disp pred) second))

(defn- check-args-length [disp args]
  ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2)))

(defn- args-are? [disp args]
  (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp))))

(defn- check-dispatch-on-args [disp args]
  (if (-> disp first vector?)
    (-> disp (args-are? args))
    (-> disp first (apply args))))

(defn- disp*args? [disp args]
  (and (check-args-length disp args)
    (check-dispatch-on-args disp args)))

(defn- args->poly-fn [poly args]
  (-> poly (get-disp #(disp*args? % args)) second))

接下来,让我们用一些初始化和设置函数来准备我们的定义宏:

(defn- poly-impl [poly args]
  (if-let [poly-fn (-> poly (args->poly-fn args))]
    (-> poly-fn (apply args))
    (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))]
      (-> default-poly-fn (apply args))
      (throw (ex-info (str "No poly for " poly " with " args) {})))))

(defn- remove-disp [poly pred]
  (when-let [disp (pred->disp poly pred)]
    (swap! polies update-in [poly :dispatch] #(->> % (remove #{disp}) vec))))

(defn- til& [args]
  (count (take-while (partial not= '&) args)))

(defn- add-disp [poly poly-fn pred params]
  (swap! polies update-in [poly :dispatch]
    #(-> % (or []) (conj [pred poly-fn (til& params) (filter #{'&} params)]))))

(defn- setup-poly [poly poly-fn pred params]
  (remove-disp poly pred)
  (add-disp poly poly-fn pred params)
  (sort-disp poly))

有了这个,我们终于可以通过在那里擦一些宏观果汁来建立我们的政策:

(defmacro defpoly [poly-name pred params body]
  `(do (when-not (-> ~poly-name quote resolve bound?)
         (defn ~poly-name [& args#] (poly-impl ~poly-name args#)))
     (let [poly-fn# (fn ~(symbol (str poly-name "-poly")) ~params ~body)]
       (setup-poly ~poly-name poly-fn# ~pred (quote ~params)))
     ~poly-name))

现在您可以构建任意谓词调度:

;; use defpoly like defmethod, but without a defmulti declaration
;;   unlike defmethods, all params are passed to defpoly's predicate function
(defpoly myinc number? [x] (inc x))

(myinc 1)
;#_=> 2

(myinc "1")
;#_=> Execution error (ExceptionInfo) at user$poly_impl/invokeStatic (REPL:6).
;No poly for user$eval187$myinc__188@5c8eee0f with ("1")

(defpoly myinc :poly/default [x] (inc x))

(myinc "1")
;#_=> Execution error (ClassCastException) at user$eval245$fn__246/invoke (REPL:1).
;java.lang.String cannot be cast to java.lang.Number

(defpoly myinc string? [x] (inc (read-string x)))

(myinc "1")
;#_=> 2

(defpoly myinc
  #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?))
  [x y & z]
  (inc (apply + x y z)))

(myinc 1 2 3)
;#_=> 7

(myinc 1 2 3 "4")
;#_=> Execution error (ArityException) at user$poly_impl/invokeStatic (REPL:5).
;Wrong number of args (4) passed to: user/eval523/fn--524

; ^ took the :poly/default path

在使用您的示例时,我们可以看到:

(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)))

(defpoly take-turn (fn [b _] (-> b keys set (contains? :direction)))
  [this world]
  (println "Directed turn!"))

;; or, if you'd rather use spec
(defpoly take-turn (fn [b _] (->> b (s/valid? (s/keys :req-un [::direction])))
  [this world]
  (println "Directed turn!"))

(take-turn (new-directed-bacterium [0 0] nil) nil)
;#_=> Directed turn!
;nil

(defpoly take-turn :poly/default [this world]
  (println "Aimless turn!"))

(take-turn (new-bacterium [0 0]) nil)
;#_=> Aimless turn!
;nil

(defpoly take-turn #(-> %& first :show) [this world]
  (println :this this :world world))

(take-turn (assoc (new-bacterium [0 0]) :show true) nil)
;#_=> :this {:mass 0, :position [0 0], :show true} :world nil
;nil

现在,让我们尝试使用isa?关系,例如defmulti

(derive java.util.Map ::collection)

(derive java.util.Collection ::collection)

;; always wrap classes in a vector to dispatch off of isa? relationships
(defpoly foo [::collection] [c] :a-collection)

(defpoly foo [String] [s] :a-string)

(foo [])
;#_=> :a-collection

(foo "bob")
;#_=> :a-string

当然,我们可以prefer用来消除关系的歧义:

(derive ::rect ::shape)

(defpoly bar [::rect ::shape] [x y] :rect-shape)

(defpoly bar [::shape ::rect] [x y] :shape-rect)

(bar ::rect ::rect)
;#_=> :rect-shape

(prefer bar [::shape ::rect] [::rect ::shape])

(bar ::rect ::rect)
;#_=> :shape-rect

再次,世界是你的牡蛎!没有什么能阻止你向任何你想要的方向扩展语言。

于 2018-11-17T19:52:07.093 回答