Диспетчеризация вызовов функций на разных форматах карт - PullRequest
0 голосов
/ 16 ноября 2018

Я пишу клон agar.io.В последнее время я видел много предложений по ограничению использования записей (например, здесь ), поэтому я пытаюсь сделать весь проект только с использованием базовых карт. *

IВ итоге мы создали конструкторы для различных "типов" бактерий, таких как

(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, так как я просто использую простые карты.

A 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 (в основном наследование).Оригинальная запись реализовала протоколы, и я не хотел делать что-то вроде вложения исходной записи в новую и маршрутизации всего поведения во вложенный экземпляр.Каждый раз, когда я создавал новый тип или изменял протокол, мне приходилось менять всю маршрутизацию, что было большой работой.

Ответы [ 3 ]

0 голосов
/ 16 ноября 2018

Диспетчеризация мультиметода полем :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!"))
0 голосов
/ 17 ноября 2018

Быстрые пути существуют по определенной причине, но 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))

С этим мы наконец можем построить наши polies, потерев туда немного сока макроса:

(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

Опять мир, твоя устрица! Ничто не мешает вам расширять язык в любом направлении.

0 голосов
/ 16 ноября 2018

Для этого вы можете использовать примерную множественную рассылку , как описано в в этом сообщении .Это, конечно, не самый эффективный способ решения этой проблемы, но, возможно, более гибкий, чем мульти-методы, поскольку он не требует объявления метода отправки заранее.Так что он открыт для расширения любого представления данных, даже других вещей, кроме карт.Если вам нужна производительность, то, вероятно, вам нужно использовать несколько методов или протоколов.

Во-первых, вам нужно добавить зависимость от [bluebell/utils "1.5.0"] и потребовать [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.Эти значения используются для определения того факта, что directed-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)

Здесь ::ebmd/any-arg является аргументомспецификация, которая соответствует любому аргументу.Приведенный выше подход открыт для расширения, как и для нескольких методов, но не требует, чтобы вы объявили поле :type заранее, и поэтому он более гибок.Но, как я уже сказал, он будет работать медленнее, чем мультиметоды и протоколы, поэтому в конечном итоге это компромисс.

Вот полное решение: https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj

...