Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?
like image 651
newBieDev Avatar asked Jul 01 '20 19:07

newBieDev


People also ask

What is a record in Clojure?

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:

What is a map in CL Clojure?

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.

Should I leave Clojure for the high-performing data structures?

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.

Is there dependency injection in Clojure?

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.


2 Answers

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

like image 99
Sean Corfield Avatar answered Oct 09 '22 20:10

Sean Corfield


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.

like image 30
Alan Thompson Avatar answered Oct 09 '22 21:10

Alan Thompson