Created
January 26, 2012 20:43
-
-
Save david-mcneil/1684980 to your computer and use it in GitHub Desktop.
Creating a custom Clojure map type
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yup, 4 year old post, but still really useful! Thanks for this!