Monday, April 12, 2010

Clojure: Converting a Custom Collection to a Sequence

I've been doing a bit of clojure lately and I've often found myself looking to convert a custom collection to a sequence. Unfortunately, several custom collections from libraries that we use don't implement Iterable; therefore, I can't simply use the seq function to create a seq.

In Java it's common to simply use a for loop and a get method to iterate through custom collections that don't implement Iterable.
for (int i=0; i<fooCollection.size(); i++) {
Foo foo = fooCollection.get(i);
}
There are patterns for doing something similar in Clojure; however, I much prefer to work with Sequences and functions that operate on Sequences. Shane Harvie and I added the following snippet to our code to quickly convert a custom collection to a seq.
(defmacro obj->seq [obj count-method]
`(loop [result# []
i# 0]
(if (< i# (. ~obj ~count-method))
(recur (conj result# (.get ~obj i#)) (inc i#))
result#)))
Our first version only worked with a custom collection that had a .size() method; however, the second collection we ran into used a .length() method, so we created the above macro that allows you to specify the (size|length|count|whatever) method.

The obj->seq function can be used similar to the following example.
(let [positions (obj->seq position-collection size)]
; do stuff with positions
)
I'm fairly new to Clojure, so please feel free to provide recommendations and feedback.

5 comments:

  1. You could do this without macros by implementing a collection-size function that selected an implementation based on the type passed in. Then use a conversion fn that calls collection-size. Advantages: (1) don't need macros, and (2) callers don't have to pass in the name of the count method.

    In 1.2, protocols provide a sexy way to deal with this, see the patch at http://www.assembla.com/spaces/clojure/tickets/289-add-internalreduce.

    ReplyDelete
  2. You can use lazy sequences to provide a more concise and performant solution:

    (defmacro obj->seq [obj count-method]
    `(map #(.get ~obj %) (range (. ~obj ~count-method))))

    ReplyDelete
  3. Cool thanks Stu and Jürgen

    ReplyDelete
  4. Anonymous7:49 PM

    You might not need a macro, as there doesn't appear to be a need for syntactic extension.

    ...building off of Jurgen's example....

    (defn obj->seq [obj count-method]
    (map #(.get obj %) (range (. obj count-method))))

    ReplyDelete
  5. This approach won't work because the symbol count-method will be resolved when calling obj->seq

    ReplyDelete

Note: Only a member of this blog may post a comment.