I'm writing an agar.io clone. I've lately seen a lot of suggestions to limit use of records (like here), so I'm trying to do the whole project only using basic maps.*
I ended up creating constructors for different "types" of bacteria like
(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)))
The "directed bacterium" has a new entry added to it. The :direction
entry will be used to remember what direction it was heading in.
Here's the problem: I want to have one function take-turn
that accepts the bacterium and the current state of the world, and returns a vector of [x, y]
indicating the offset from the current position to move the bacterium to. I want to have a single function that's called because I can think right now of at least three kinds of bacteria that I'll want to have, and would like to have the ability to add new types later that each define their own take-turn
.
A Can-Take-Turn
protocol is out the window since I'm just using plain maps.
A take-turn
multimethod seemed like it would work at first, but then I realized that I'd have no dispatch values to use in my current setup that would be extensible. I could have :direction
be the dispatch function, and then dispatch on nil
to use the "directed bacterium"'s take-turn
, or default to get the base aimless behavior, but that doesn't give me a way of even having a third "player bacterium" type.
The only solution I can think of it to require that all bacterium have a :type
field, and to dispatch on it, like:
(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
But now I'm back to basically dispatching on type, using a slower method than protocols. Is this a legitimate case to use records and protocols, or is there something about mutlimethods that I'm missing? I don't have a lot of practice with them.
*
I also decided to try this because I was in the situation where I had a Bacterium
record and wanted to create a new "directed" version of the record that had a single field direction
added to it (inheritance basically). The original record implemented protocols though, and I didn't want to have to do something like nesting the original record in the new one, and routing all behavior to the nested instance. Every time I created a new type or changed a protocol, I would have to change all the routing, which was a lot of work.
Fast paths exist for a reason, but Clojure doesn't stop you from doing anything you want to do, per say, including ad hoc predicate dispatch. The world is definitely your oyster. Observe this super quick and dirty example below.
First, we'll start off with an atom to store all of our polymorphic functions:
In usage, the internal structure of the
polies
would look something like this:Now, let's make it so that we can
prefer
predicates (likeprefer-method
):Now, let's create our dispatch lookup system:
Next, let's prepare our define macro with some initialization and setup functions:
With that, we can finally build our polies by rubbing some macro juice on there:
Now you can build arbitrary predicate dispatch:
And when using your example, we can see:
Now, let's try using
isa?
relationships, a ladefmulti
:And of course we can use
prefer
to disambiguate relationships:Again, the world's your oyster! There's nothing stopping you from extending the language in any direction you want.