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:
A record can be defined like this: This creates an actual normal Java class Person with two immutable fields and implements hashCode and equals. A record also behaves like map which something you’ll appreciate after learning Clojure! Notice that although Clojure is dynamically typed you can add something called Type Hints if you need them:
Clojure - Maps. A Map is a collection that maps keys to values. Two different map types are provided - hashed and sorted. HashMaps require keys that correctly support hashCode and equals. SortedMaps require keys that implement Comparable, or an instance of Comparator.
The intent is that, unless interop forces one to go beyond their circumscribed scope, one need not leave Clojure to get the highest-performing data structures possible on the platform.
Since Clojure is functional we strive to be pure and to have no side-effects. A void method always means that there are side-effects! Implementing a protocol can be done using a record like this: And finally dependency injection. The short answer is that it is not as needed as in Java.
This flowchart is still good advice, nine years later: https://cemerick.com/2011/07/05/flowchart-for-choosing-the-right-clojure-type-definition-form/ -- seems to have moved to https://cemerick.com/blog/2011/07/05/flowchart-for-choosing-the-right-clojure-type-definition-form.html
My rule of thumb is: always use plain hash maps until you really need polymorphism and then decide whether you want multi-methods (dispatch on one or more arguments/attributes) or protocols (dispatch on just the type).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With