Copper Thoughts

Initial Thoughts on Clojure

Though I've been playing with Clojure on and off during the last six months or so, it's just been in the last month or so that I've really had a chance to dig into the language, and the culture. For the most part, I've really enjoyed the whole experience, and I plan to keep enjoying it in the foreseeable future.

One of my favorite parts of Clojure is its sequence abstraction and the large library of sequence functions that are available, standard, to work with it. This includes the invaluable lazy sequences, which allow for some shenanigans that might be less expressible in other languages with fully strict evaluation. It is sequences that I first want to write about now.

I recently found myself needing a way to get the index of value in a list. It is something that's standard in many languages: index() in Python, indexOf() in Java, findIndex in Haskell… the list (hah!) goes on. Clojure doesn't have such a function, and I suppose that's alright, as I haven't ever needed it. List comprehensions and functions like filter generally take care of all such cases in more concise ways than the index functions do, though it's rare that you find yourself actually needing those functions in Python or Haskell anyway.

I still don't need it, in fact, but it came about quite naturally in my utils.clj mashup today as a result of a simple, neat function I did in fact need; satisfactory-index.

Here are the functions themselves:

(defn satisfactory-indices
     "Returns a lazy sequence of indices of vals in 'coll for which (pred val)
      is truthy."
      [pred col]
      (filter identity
              (map-indexed
               (fn [index item]
                 (if (pred item) index nil))
               col)))

(defn satisfactory-index
      "Returns the index of the first value in 'coll for which (pred val) is truthy."
       [pred coll]
       (first (satisfactory-indices pred coll)))

This is the version that I came up with after rediscovering map-indexed, thanks to Chas Emerick. The previous version isn't worth posting (though I do post one version of it below, for different reasons), as it basically re-implements map-indexed. As both map-indexed and filter are lazy, we get a lazy satisfactory-indices for free, which means we get an efficient satisfactory-index for free as well.

This function is very versatile thanks to its laziness. It is trivial to implement indices-of and index-of, as well, due to the fact that functions are first class and we can pass and compose them with grace and ease!

(defn indices-of
  "Returns a lazy-seq of indices where 'val appears in 'coll."
  [val coll]
  (satisfactory-indices #(= val %) coll))
(defn index-of
  "Returns the first index where 'val appears in 'coll."
  [val col]
  (first (indices-of val col)))

I've really been enjoying working and playing with Clojure. There are some warts though, most notably the abuse (use, really, but I feel like I'm just flogging Java with a Lisp bat when I do it) of the really nice inter-op between Clojure and Java. How many libraries have I used (or created!?) that just wrap Java functions with convenient Clojure-native syntax? Way too many. But there really are a lot of libraries in Clojure, and the need for Java is shrinking, and the occasional use of it is far from unpleasant. Writing Java in Clojure is often a more pleasant experience than writing Java in Java.

Something I encountered today which made me cringe just a bit are the restrictions in place regarding recur versus just using the name of a function itself.

While writing my re-implementation of map-indexed, I attempted to write a solution like so:

(defn indices-of
      [val coll]
      (lazy-seq
       (loop [coll coll idx 0]
         (cond
          (empty? coll) '()
          (= val (first coll)) (cons idx (recur val (rest coll) (inc idx)))
          :else (recur val (rest coll) (inc idx))))))

I quickly realized that just wouldn't fly: recur is not in the tail position here, so the Clojure compiler can't figure out how to place the goto it needs to in the compiled bytecode. That's actually not a big deal, as I can use letfn to create a local function and use that for my recursion, like so:

(defn indices-of
      [val coll]
      (letfn [(f [coll idx]
             (lazy-seq 
               (cond
                 (empty? coll) '()
                 (= val (first coll)) (cons idx (f (rest coll) (inc idx))
                 :else (f (rest coll) (inc idx)))))
        (f col 0)))

I guess what I really want is a named let [1], so I could write something like this:

(defn indices-of
     [val coll]
     (letn f [coll coll idx 0]
             (lazy-seq 
               (cond
                 (empty? coll) '()
                 (= val (first coll)) (cons idx (f (rest coll) (inc idx)))
                 :else (f (rest coll) (inc idx)))))))

Which, aside from basically reimplementing map-indexed with f, seems to be the most desirable solution. Alternatively, a named loop-recur construct, that would allow for non-tail-calling recurring might also solve the problem, though it's not really much of a problem.

Honestly, I've never had as much fun programming as I have in programming Clojure. There's very little ceremony involved in getting things done, and it's definitely got a killer IDE in Emacs, Leiningen, & Slime/Swank (be sure to import clojure.repl!) I wouldn't recommend it as a first language, though: right now, getting started is a bit rocky if you don't know your way around a computer very well.

As my Junior year in college begins, giving me more time to work on contributing to the community that has given so much to me, I hope to post here more about Clojure. There are a number of fantastic projects I'd love to sink some time into, and I want to highlight them in the coming months on this blog, and to eventually relaunch this blog as one powered by Clojure [2].

For now, it's more data analysis, and creating GUIs for interacting with said data, all in Clojure. Life's pretty good.

[1] Other Lips, like Scheme, have this already, so it's not at all a new concept. It's just not particularly needed in Clojure, but I do think it'd be a welcome addition.

[2] I actually do have a blogging engine written in Clojure, using the Enlive library, but it just wasn't as awesome as I'd have liked. I plan on rewriting, using what I've learned over the past month of pretty intense use.

blog comments powered by Disqus