Instantly share code, notes, and snippets.

Embed
What would you like to do?
Creating a custom Clojure map type
(ns people
(:use [clojure.string :only (join)]
[clojure.pprint :only (pprint simple-dispatch)]))
;; we can make maps using the special literal form:
{:a 100
:b 200}
(class {:a 100 :b 200})
;;=> clojure.lang.PersistentArrayMap
;; we can make maps using the explicit constructor form:
(hash-map :a 100 :b 200)
(class (hash-map :a 100 :b 200))
;;=> clojure.lang.PersistentHashMap
(class (array-map :a 100 :b 200))
;;=> clojure.lang.PersistentArrayMap
;; let's make our own map type to represent people
;; our map will have default values:
(def default-contents {:species "human"
:status :alive})
;; whatever contents are provided at construction time will be
;; augmented with the default values
(defn augment-contents [contents]
(merge default-contents contents))
;; deftype is used to create our own type
(deftype Person [contents]
;; ...
)
;; the type exists
(Person. {:name "fred"})
;;=> #<Person people.Person@427b9969>
(class (Person. {:name "fred"}))
;;=> people.Person
;; but it doesn't work as a map yet
(:name (Person. {:name "fred"}))
;;=> nil
;; we can look at the Map classes (i.e. the Java source code for them)
;; above to get an idea for what we need to implement
;; the first interface we find that we need to implement is IPersistentMap
(deftype Person [contents]
clojure.lang.IPersistentMap
(assoc [_ k v]
(Person. (.assoc contents k v)))
(assocEx [_ k v]
(Person. (.assocEx contents k v)))
(without [_ k]
(Person. (.without contents k))))
(Person. {:x 100})
;;=> AbstractMethodError
;; hmm... seems we need to implement some more
;; after tracking down more of the Clojure and Java interfaces that
;; PersistentArrayMap implements, we end up with this:
(deftype Person [contents]
clojure.lang.IPersistentMap
(assoc [_ k v]
(Person. (.assoc contents k v)))
(assocEx [_ k v]
(Person. (.assocEx contents k v)))
(without [_ k]
(Person. (.without contents k)))
java.lang.Iterable
(iterator [this]
(.iterator (augment-contents contents)))
clojure.lang.Associative
(containsKey [_ k]
(.containsKey (augment-contents contents) k))
(entryAt [_ k]
(.entryAt (augment-contents contents) k))
clojure.lang.IPersistentCollection
(count [_]
(.count (augment-contents contents)))
(cons [_ o]
(Person. (.cons contents o)))
(empty [_]
(.empty (augment-contents contents)))
(equiv [_ o]
(and (isa? (class o) Person)
(.equiv (augment-contents contents) (.(augment-contents contents) o))))
clojure.lang.Seqable
(seq [_]
(.seq (augment-contents contents)))
clojure.lang.ILookup
(valAt [_ k]
(.valAt (augment-contents contents) k))
(valAt [_ k not-found]
(.valAt (augment-contents contents) k not-found)))
;; the type is no longer broken
(Person. {:name "fred"})
;;=> {:status :alive, :name "fred", :species "human"}
;; as far as Clojure is concerned, it is a map
(map? (Person. {:name "fred"}))
;;=> true
;; we can access it as a map
(:name (Person. {:name "fred"}))
;;=> "fred"
;; the default field values are present
(:species (Person. {:name "fred"}))
;;=> "human"
;; we can assoc into it
(assoc (Person. {:name "fred"}) :age 20)
;;=> {:age 20, :status :alive, :name "fred", :species "human"}
;; without losing it's Person-hood
(class (assoc (Person. {:name "fred"}) :age 20))
;;=> people.Person
;; we can destructure it as a map
(let [{:keys (name age)} (Person. {:name "fred" :age 20})]
[name age])
;;=> ["fred" 20]
;; besides calling the class name directly
;; we can use the Clojure 1.3 literal Java object syntax
(java.util.Date. 100)
;;=> #<Date Wed Dec 31 18:00:00 CST 1969>
#java.util.Date[100]
;;=> #<Date Wed Dec 31 18:00:00 CST 1969>
;; the positional literal syntax works with our new type
#people.Person[{:name "joe"}]
;;=> {:status :alive, :name "joe", :species "human"}
;; there is also a labelled syntax
(defrecord Foo [a b])
;; which records print as by default
(Foo. 100 200)
;;=> #people.Foo{:a 100, :b 200}
#people.Foo {:a 100 :b 200}
;;=> Unreadable constructor form starting with "#people.Foo "
;; watch the spaces
#people.Foo{:a 100 :b 200}
;;=> #people.Foo{:a 100, :b 200}
;; alas, the labelled syntax doesn't work for our new type :(
#people.Person{:contents {:name "joe"}}
;;=> No matching method found: create
;; looks like something else to track down, but let's ignore it for now
;; So we have a couple of ways of making a new Person
;; but these syntaxes are a bit ugly for daily use, let's make a
;; nicer looking constructor function
(defn new-person [& raw-contents]
(Person. (apply hash-map raw-contents)))
(new-person :name "fred")
;;=> {:status :alive, :name "fred", :species "human"}
;; currently Person objects print as simple maps
;; this means that if we eval their printed form, we lose their
;; Person-hood
(with-out-str (pr (new-person :name "fred")))
;;=> "{:status :alive, :name \"fred\", :species \"human\"}"
(read-string (with-out-str (pr (new-person :name "fred"))))
;;=> {:status :alive, :name "fred", :species "human"}
(class (read-string (with-out-str (pr (new-person :name "fred")))))
;;=> clojure.lang.PersistentArrayMap
;; to address this we will setup our new type to print a form that uses our constructor function
(do
;; setup-printing
;; this function knows how to print a Person object on a Writer
;; we will only print the explicit contents, not the default values
(defn print-person [p writer]
(.write writer (str (apply list (into ['people/new-person]
(mapcat identity (.contents p)))))))
;; we can delegate to print-person from the various print hooks in
;; Clojure
(defmethod print-method Person [p writer]
(print-person p writer))
(defmethod print-dup Person [p writer]
(print-person p writer))
(.addMethod simple-dispatch Person (fn [p]
(print-person p *out*))))
(new-person :name "fred")
;;=> (people/new-person :name "fred")
;; if we call print-str strings don't print right
(print-str (new-person :name "fred"))
;;=> "(people/new-person :name fred)"
;; if we set *print-dup* true then it will print in a form that can be
;; read back in
(binding [*print-dup* true]
(print-str (new-person :name "fred")))
;;=> "(people/new-person :name \"fred\")"
;; the printed form can be read back in by Clojure
(read-string "(people/new-person :name \"fred\")")
;;=> (people/new-person :name "fred")
;; however it produces a list, not a Person
(class (read-string "(people/new-person :name \"fred\")"))
;;=> clojure.lang.PersistentList
;; there is no reader magic for our constructor function
;; so we have to call eval
(eval (read-string "(people/new-person :name \"fred\")"))
;;=> (people/new-person :name "fred")
(class (eval (read-string "(people/new-person :name \"fred\")")))
;;=> people.Person
;; that points to an advantage of the literal Java object syntax
;; it reads in as an object of our type
(class (read-string "#people.Person[{:name \"joe\"}]"))
;;=> people.Person
@BroiSatse

This comment has been minimized.

Show comment
Hide comment
@BroiSatse

BroiSatse Feb 8, 2016

The only reason for abstract method error is the way repl is displaying your structure, so (Person. {:x 100}) will raise an exception, but (def person (Person. {:x 100}) won't. That can be easily fixed by not implementing IPersistentMap (which, when implemented forced repl to print object as a map) and implement clojure.lang.Associative instead. Without IPersistentMap repl presenter will fall back to visit-unknown without iterating, counting and other unnecessary stuff.

(I am aware it is 4 years old post. :))

BroiSatse commented Feb 8, 2016

The only reason for abstract method error is the way repl is displaying your structure, so (Person. {:x 100}) will raise an exception, but (def person (Person. {:x 100}) won't. That can be easily fixed by not implementing IPersistentMap (which, when implemented forced repl to print object as a map) and implement clojure.lang.Associative instead. Without IPersistentMap repl presenter will fall back to visit-unknown without iterating, counting and other unnecessary stuff.

(I am aware it is 4 years old post. :))

@pdenno

This comment has been minimized.

Show comment
Hide comment
@pdenno

pdenno Mar 6, 2016

Yup, 4 year old post, but still really useful! Thanks for this!

pdenno commented Mar 6, 2016

Yup, 4 year old post, but still really useful! Thanks for this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment