Clojure multimethods, how to add data?

341 views Asked by At

Firstly, I'm new to Clojure, so this is likely to be stupid question.

As a learning exercise, I've got a trivial text adventure multimethod system working. I now want to change from using keywords to some form of 'classiness', which can hold data pertaining to the individual instances of 'sack', 'sword', etc.

Is defrecord the way to go here?

The question: Can I use Clojure's derive to create a hierarchy of my defrecord class types? seem similar to this, but the accepted answer says 'no, perhaps use interfaces'.

Is the answer really no? Do I have to write all the data representations as Java classes in order to use Clojure's multimethods?

Thanks,

Chris.

Working code:

(derive ::unlit_root ::room)
(derive ::room ::thing)
(derive ::item ::thing)
(derive ::sword ::item)
(derive ::container ::thing)
(derive ::sack ::container)
(derive ::sack ::item)
(derive ::wardrobe ::furniture)
(derive ::furniture ::thing)
(derive ::wardrobe ::furniture)

(defmulti put (fn [x y z] [x y z]))
(defmethod put [::room ::thing ::thing] [x y z] "you can only put items into containers")
(defmethod put [::room ::sword ::sack] [x y z] "the sword cuts the sack")
(defmethod put [::room ::item ::container] [x y z] "ordinary success")
(defmethod put [::unlit_room ::thing ::thing] [x y z] "it's too dark, you are eaten by a grue")
(defmethod put [::room ::sack ::wardrobe] [x y z] "you win")
(defmethod put [::room ::item ::sack] [x y z] "you put it in the sack")
(defmethod put [::room ::furniture ::thing] [x y z] "it's too big to move")

Below, is what I've tried so far, but I get an error at the first derive:

ClassCastException java.lang.Class cannot be cast to clojure.lang.Named clojure.core/namespace (core.clj:1496).

(defrecord Item [name])
(defrecord Weapon [name, damage])
(defrecord Furniture [name])
(defrecord Container [name])
(defrecord Bag [name])
(derive Weapon Item)
(derive Container Item)
(derive Bag Container)
(derive Furniture Container)

(def sword (Weapon. "sword" 10))
(def apple (Item. "apple"))
(def cupboard (Furniture. "cupboard"))
(def bag (Bag. "bag"))


(defmulti putin (fn [src dst] [src dst]))
(defmethod putin [Item Container] [src dst] :success_0)
3

There are 3 answers

3
Beyamor On BEST ANSWER

The unhappy answer, as @Arthur and @noahz mentioned, is that hierarchies can't be described with classes. Where does that leave us with multimethods?

The best answer might be including a :type key in simple maps and dispatching on that value. You lose things like the auto-generated constructor afforded by protocols, but it's a very straightforward solution and it offers a lot of flexibility.

(def sword {:type ::weapon, :name "sword", :damage 10})
(def apple {:type ::item, :name "apple"})
(def cupboard {:type ::furniture, :name "cupboard"})
(def bag {:type ::bag, :name "bag"})

(derive ::weapon ::item)
(derive ::container ::item)
(derive ::bag ::container)
(derive ::furniture ::container)

; dispatch on [type-of-src type-of-dst]
(defmulti putin (fn [src dst] [(src :type) (dst :type)]))
(defmethod putin [::item ::container] [src dst] :success_0)

(println (putin sword bag)) ; :success_0

An alternative, albeit one that suffers from over-complication, is to create a map of classes to keywords and use this to look up keywords in the hierarchy when dispatching. Again, I'll stress that you can probably find something better, but the option is there.

; used to look up the keywords associated with classes
(def class-keyword-map (atom {}))

; get the keyword associated with an instance's class
(defn class-keyword
  [instance]
  (@class-keyword-map (class instance)))

; this macro defines a record as normal
; however, after defining the record,
; it associates the record's type with
; a keyword generated by the record name
(defmacro def-adventure-record
  [clazz & body]
  `(do
     ; create the record as normal
     (defrecord ~clazz ~@body)
     ; and add the type to the keyword lookup
     (swap!
       class-keyword-map
       assoc ~clazz (keyword (str *ns*) (str '~clazz)))))

(def-adventure-record Item [name])
(def-adventure-record Weapon [name, damage])
(def-adventure-record Furniture [name])
(def-adventure-record Container [name])
(def-adventure-record Bag [name])

; we still need to use keywords,
; but at this point, they've been
; generated for us by the macro above
(derive ::Weapon ::Item)
(derive ::Container ::Item)
(derive ::Bag ::Container)
(derive ::Furniture ::Container)

(def sword (Weapon. "sword" 10))
(def apple (Item. "apple"))
(def cupboard (Furniture. "cupboard"))
(def bag (Bag. "bag"))

; this dispatch is done on the class's keywords
(defmulti putin (fn [src dst] [(class-keyword src) (class-keyword dst)]))

; again, keywords describe the multimethod
(defmethod putin [::Item ::Container] [src dst] :success_0)

(println (putin sword bag)) ; :success_0
0
Arthur Ulfeldt On

Clojure offers both protocols and multimethods for solving this sort of problem. If you want to use defrecord then I would recommend goind with protocols instead.


the specific problem is explained in the multimethod page:

"You can also use a class as the child (but not the parent, the only way to make something the child of a class is via Java inheritance)."

(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)

You can continue to use isa? hierarchies with

0
noahlz On

You want to bring Java's type system into Clojure. The way you do that (in the manner you're seeking) is with Protocols (See also http://clojure.org/protocols)

However, I recommend you read the following blog post: Rifle-Oriented Programming with Clojure. Consider perhaps that data structures are perhaps good enough (and more flexible) than using types.