views:

581

answers:

6

OK, no cheating now.

No, really, take a minute or two and try this out.

What does "positions" do?

Edit: simplified according to cgrand's suggestion.

(defn redux [[current next] flag] [(if flag current next) (inc next)])

(defn positions [coll]
  (map first (reductions redux [1 2] (map = coll (rest coll)))))

Now, how about this version?

def positions(coll) {
  def (current, next) = [1, 1]
  def previous = coll[0]
  coll.collect {
    current = (it == previous) ? current : next
    next++
    previous = it
    current
  }
}

I'm learning Clojure and I'm loving it, because I've always enjoyed functional programming. It took me longer to come up with the Clojure solution, but I enjoyed having to think of an elegant solution. The Groovy solution is alright, but I'm at the point where I find this type of imperative programming boring and mechanical. After 12 years of Java, I feel in a rut and functional programming with Clojure is the boost I needed.

Right, get to the point. Well, I have to be honest and say that I wonder if I'll understand the Clojure code when I go back to it months later. Sure I could comment the heck out of it, but I don't need to comment my Java code to understand it.

So my question is: is it a question of getting more used to functional programming patterns? Are functional programming gurus reading this code and finding it a breeze to understand? Which version did you find easier to understand?

Edit: what this code does is calculate the positions of players according to their points, while keep track of those who are tied. For example:


Pos Points
1. 36
1. 36
1. 36
4. 34
5. 32
5. 32
5. 32
8. 30
+9  A: 

I don't think there's any such thing as intrinsic readability. There's what you're used to, and what you aren't used to. I was able to read both versions of your code OK. I could actually read your Groovy version more easily, even though I don't know Groovy, because I too spent a decade looking at C and Java and only a year looking at Clojure. That doesn't say anything about the languages, it only says something about me.

Similarly I can read English more easily than Spanish, but that doesn't say anything about the intrinsic readability of those languages either. (Spanish is actually probably the "more readable" language of the two in terms of simplicity and consistency, but I still can't read it). I'm learning Japanese right now and having a heck of a hard time, but native Japanese speakers say the same about English.

If you spent most of your life reading Java, of course things that look like Java will be easier to read than things that don't. Until you've spent as much time looking at Lispy languages as looking at C-like languages, this will probably remain true.

To understand a language, among other things you have to be familiar with:

  • syntax ([vector] vs. (list), hyphens-in-names)
  • vocabulary (what does reductions mean? How/where can you look it up?)
  • evaluation rules (does treating functions as objects work? It's an error in most languages.)
  • idioms, like (map first (some set of reductions with extra accumulated values))

All of these take time and practice and repetition to learn and internalize. But if you spend the next 6 months reading and writing lots of Clojure, not only will you be able to understand that Clojure code 6 months from now, you'll probably understand it better than you do now, and maybe even be able to simplify it. How about this:

(use 'clojure.contrib.seq-utils)                                        ;;'
(defn positions [coll]
  (mapcat #(repeat (count %) (inc (ffirst %)))
          (partition-by second (indexed coll))))

Looking at Clojure code I wrote a year ago, I'm horrified at how bad it is, but I can read it OK. (Not saying your Clojure code is horrible; I had no trouble reading it at all, and I'm no guru.)

Brian Carper
Excellent response. I love your version. I'll admit it took me a while to get it. It's quite clever! Thanks.
Frederic Daoud
+6  A: 

edit: may not be relevant anymore.

The Clojure one is convoluted to me. It contains more abstractions which need to be understood. This is the price of using higher order functions, you have to know what they mean. So in an isolated case, imperative requires less knowledge. But the power of abstractions is in their means of combination. Every imperative loop must be read and understood, whereas sequence abstractions allow you to remove the complexity of a loop and combine powerful opperations.

I would further argue that the Groovy version is at least partially functional as it uses collect, which is really map, a higher order function. It has some state in it also.

Here is how I would write the Clojure version:

(defn positions2 [coll]
  (let [current (atom 1)
        if-same #(if (= %1 %2) @current (reset! current (inc %3)))]
    (map if-same (cons (first coll) coll) coll (range (count coll)))))

This is quite similar to the Groovy version in that it uses a mutable "current", but differs in that it doesn't have a next/prev variable - instead using immutable sequences for those. As Brian elloquently put it, readability is not intrinsic. This version is my preference for this particular case, and seems to sit somewhere in the middle.

Timothy Pratley
I edited the code. I don't think it's too convoluted now.
Frederic Daoud
Cool :) It was not a criticism of your code, I was just answering the question about which style was easier to understand. Now the goal posts have moved! Hehehe.
Timothy Pratley
Well I didn't take it as criticism, and besides, you were right, the first version /was/ convoluted. :-)
Frederic Daoud
+7  A: 

I agree with Timothy: you introduce too much abstractions. I reworked your code and ended with:

(defn positions [coll]
  (reductions (fn [[_ prev-score :as prev] [_ score :as curr]] 
                (if (= prev-score score) prev curr))
    (map vector (iterate inc 1) coll)))

About your code,

(defn use-prev [[a b]] (= a b))
(defn pairs [coll] (partition 2 1 coll))
(map use-prev (pairs coll))

can be simply refactored as:

(map = coll (rest coll))
cgrand
Thanks, I edited above according to your refactoring suggestion.
Frederic Daoud
+2  A: 

I too am learning Clojure and loving it. But at this stage of my development, the Groovy version was easier to understand. What I like about Clojure though is reading the code and having the "Aha!" experience when you finally "get" what is going on. What I really enjoy is the similar experience that happens a few minutes later when you realize all of the ways the code could be applied to other types of data with no changes to the code. I've lost count of the number of times I've worked through some numerical code in Clojure and then, a little while later, thought of how that same code could be used with strings, symbols, widgets, ...

The analogy I use is about learning colors. Remember when you were introduced to the color red? You understood it pretty quickly -- there's all this red stuff in the world. Then you heard the term magenta and were lost for a while. But again, after a little more exposure, you understood the concept and had a much more specific way to describe a particular color. You have to internalize the concept, hold a bit more information in your head, but you end up with something more powerful and concise.

clartaq
+2  A: 

The Clojure one is more convoluted at first glance; though it maybe more elegant. OO is the result to make language more "relatable" at higher-level. Functional languages seems to have a more "algorithimc"(primitive/elementary) feel to it. That's just what I felt at the moment. Maybe that will change when I have more experience working with clojure.

I'm afraid that we are decending into the game of which language can be the most concise or solve a problem in the least line of code.

The issue are 2 folds for me:

  1. How easy at first glance to get a feel of what the code is doing?. This is important for code maintainers.

  2. How easy is it to guess at the logic behind the code?. Too verbose/long-winded?. Too terse?

"Make everything as simple as possible, but not simpler."

Albert Einstein

bk
I don't think it's about the least lines of code. At least that is not my intention. The issues are rather those that you elegantly enumerated.For example, I prefer my version of the code rather than the shorter, but less readable (IMHO) version of 'gnud' (in the expanded comments) in this script: http://stackoverflow.com/questions/188162/what-is-the-most-useful-script-youve-written-for-everyday-life/245724#245724
Frederic Daoud
+1  A: 

Groovy supports various styles of solving this problem too:

coll.groupBy{it}.inject([]){ c, n -> c + [c.size() + 1] * n.value.size() }

definitely not refactored to be pretty but not too hard to understand.

Paul King