Sometimes You Feel Like a Map
Creating your own data type that behaves like a map is pretty straight forward. The tricky part is figuring out which interfaces you need to implement to enable the different parts of Clojure's map API. The following is an overview of what is necessary to make your custom type behave like a map on the JVM.
clojure.lang.Associative
The Associative interface gives you the APIs necessary to put stuff into your map and check to see that it's there.
Associative
(containsKey [this key])
(entryAt [this key])
(assoc [this key value])
Watch Out
containsKeymust returntrueorfalseor something that can be cast tojava.lang.Boolean, otherwise you'll get aClassCastExceptionentryAtshould return anIMapEntryif the key exists,nilotherwise. Your best bet is to return aclojure.lang.MapEntry.
API Calls
assoccontains?find
clojure.lang.ILookup
Where Associative lets you add to your map, ILookup provides the API for getting things out again.
ILookup
(valAt [this k])
(valAt [this k not-found])
API Calls
(:kwd foo)(:kwd foo 'default')getget-in
clojure.lang.IFn
The IFn interface is what allows your map to be called as a function (See API Calls below).
IFn
(invoke [this k])
(invoke [this k not-found])
API Calls
(your-map :kwd)(your-map :kwd 'default')
clojure.lang.IPersistentCollection
IPersistentCollection
(cons [this o])
(count [this])
(empty [this])
(equiv [this o])
Watch Out
emptyis not the same thing asempty?. It should return whatever an “empty” representation of your map is.equivwill only apply to comparing other things to your map, but not the other way. Eg.(= your-map thing)will hit your implementation ofequiv, but(= thing your-map)will not.
API Calls
conjconscountempty=
clojure.lang.IPersistentMap
Having already implemented Associative, we get a free pass on this assoc. assocEx is pretty much the same except that it throws an exception if the key already exists. without has the same behavior as dissoc.
IPersistentMap
(assoc [this k v])
(assocEx [this k v])
(without [this k])
API Calls
assocdissoc
clojure.lang.Seqable
The Seqable interface is by far the biggest bang for the buck. Implement one function and you get a sizable chunk of the map and collection APIs.
Seqable
(seq [this])
Watch Out
- Remember,
seqshould returnnilif your map is empty.
API Calls
empty?filterfirstkeysmaprestselect-keysseqsomevals- etc.
java.lang.Iterable
Seeing as you've already implemented seq, this one is a no brainer. You can simply wrap the result of calling seq on your map into a clojure.lang.SeqIterator and call it a day.
Iterable
(iterator [this])
API Calls
reducereduce-kv
Wrap Up
You should now have a custom type that you can treat just like any other map. You can find examples of other custom maps in crosshair, mapstache, and strata.
If you notice mistakes, typo or otherwise, feel free to let me know @thenandagain on Twitter, or send a pull request.