Clojure - What's the benefit of Using Records Over Maps

Viewed 1368

I'm having a hard time deciding when using defrecord is the right choice and more broadly if my use of protocols on my records is semantic clojure and functional.

In my current project I'm building a game that has different types of enemies that all have the same set of actions, where those actions might be implemented differently.

Coming from an OOP background, I'm tempted to do something like:

(defprotocol Enemy
  "Defines base function of an Enemy"
  (attack [this] "attack function"))

(extend-protocol Enemy
  Orc
  (attack [_] "Handles an orc attack")  
  Troll
  (attack [_] "Handles a Troll attack"))



(defrecord Orc [health attackPower defense])
(defrecord Troll [health attackPower defense])

(def enemy (Orc. 1 20 3))
(def enemy2 (Troll. 1 20 3))

(println (attack enemy))
; handles an orc attack

(println (attack enemy2))
;handles a troll attack

This looks like it makes sense on the surface. I want every enemy to always have an attack method, but the actual implementation of that should be able to vary on the particular enemy. Using the extend-protocol I'm able to create efficient dispatch of the methods that vary on my enemies, and I can easily add new enemy types as well as change the functionally on those types.

The problem I'm having is why should I use a record over a generic map? The above feels a bit to OOP to me, and seems like I'm going against a more functional style. So, my question is broken into two:

  1. Is my above implementation of records and protocols a sound use case?
  2. More generically, when is a record preferred over a map? I've read you should favor records when you're re-building the same map multiple times (as I would be in this case). Is that logic sound?
2 Answers

To Sean's excellent answer, I would only add that records can slow down iterative development, especially using a tool like lein-test-refresh or similar.

Records form a separate Java class, and must be recompiled upon every change, which can slow down the iteration cycle.

In addition, recompilation breaks comparison with still-existing record objects, since the recompiled object (even if there are no changes!) will not be = to the original since it has a different class file. As an example, suppose you have a Point record:

(defrecord Point [x y])

(def p (->Point 1 2)) ; in file ppp.clj
(def q (->Point 1 2)) ; in file qqq.clj

(is (= p q)) ; in a unit test

If file ppp.clj gets recompiled, it generates a new Point class with a different "ID" value than before. Since records must have the same type AND values to be considered equal, the unit test will fail even though both are of type Point and both have values [1 2]. This is an unintended pain point when using records.

Related