Skip to content

Instantly share code, notes, and snippets.

Created January 26, 2012 20:43
Show Gist options
  • Save david-mcneil/1684980 to your computer and use it in GitHub Desktop.
Save david-mcneil/1684980 to your computer and use it in GitHub Desktop.
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]
(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]
(assoc [_ k v]
(Person. (.assoc contents k v)))
(assocEx [_ k v]
(Person. (.assocEx contents k v)))
(without [_ k]
(Person. (.without contents k)))
(iterator [this]
(.iterator (augment-contents contents)))
(containsKey [_ k]
(.containsKey (augment-contents contents) k))
(entryAt [_ k]
(.entryAt (augment-contents contents) k))
(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))))
(seq [_]
(.seq (augment-contents contents)))
(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>
;;=> #<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
;; 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
Copy link

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

Copy link

pdenno commented Mar 6, 2016

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

Copy link

bhurlow commented May 17, 2019


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