Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding custom behavior to Clojure's sequences

Part of what's so powerful about Clojure is that all the core data-types implement the same sequence abstraction: clojure.lang.ISeq.

This means that functions like "first", "concat", "cons", "map", "rest", etc work generically on all of those data-types.

My question is this: how can I add my own custom function into the mix, and have it work for all the types that extend from ISeq?

One first attempt was to define my own protocol, then "(extend-type clojure.lang.ISeq ...", but that doesn't work (it compiles but doesn't add the behavior to the actual types). Another idea was to write a macro that does an "extend-type" explicitly on all the Clojure types (PersistentHashMap, PersistentList, etc), but that seems kludgey.

Is there any elegant/idiomatic way to do this? Multimethods perhaps?

like image 884
AgentLiquid Avatar asked Sep 16 '11 02:09

AgentLiquid


3 Answers

What exactly are you trying to do?

If you're trying to add behaviour to existing types: either write normal functions that deal with seqs or use multimethods or extend to do what you want.

Also, something to note is that most Clojure "sequence" types (vectors, sets, maps) are not in themselves sequences (they do not implement clojure.lang.ISeq), so you have to do more than just add to clojure.lang.ISeq if you wish to support them.

like image 78
mange Avatar answered Oct 15 '22 09:10

mange


There's an IBM Developerworks article by Stuart Sierra titled Solving the Expression Problem with Clojure 1.2 that might provide insights and an answer to your question.

It uses protocols to define a group of functions on several data types, and uses extend to extend existing classes (data types) so they can use these functions. That might not be exactly what you want, but it could be one way of solving your problem.

It also shows you how you could define custom data types (using defrecord and deftype) that implement existing protocols / interfaces.

like image 34
Gert Avatar answered Oct 15 '22 11:10

Gert


The best way to do this is to write your new functions using existing generic Clojure functions that handle the different data types correctly.

Examples of such generic functions:

  • into appends items to a collection of any type
  • empty returns an empty collection of the same type as it's parameter

Then you can write your own generic function that exploits these, e.g.:

(defn take-every-other [coll]
  (into 
    (empty coll)
    (map first (partition 2 coll))))

 (take-every-other [1 2 3 4 5 6])
 => [1 3 5]

 (take-every-other {:a 1 :b 2 :c 3 :d 4})
 => {:a 1, :c 3}

If you still need more generic function capability, you can always dive into the Clojure source to see how these functions are written.

like image 32
mikera Avatar answered Oct 15 '22 11:10

mikera