Is functional Clojure or imperative Groovy more readable?
Asked Answered
L

7

15

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
Lowly answered 13/11, 2009 at 3:27 Comment(0)
E
22

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.)

Emanation answered 13/11, 2009 at 4:52 Comment(1)
Excellent response. I love your version. I'll admit it took me a while to get it. It's quite clever! Thanks.Lowly
L
8

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.

Leveroni answered 13/11, 2009 at 7:12 Comment(2)
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.Leveroni
Well I didn't take it as criticism, and besides, you were right, the first version /was/ convoluted. :-)Lowly
S
8

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))
Sumption answered 13/11, 2009 at 12:50 Comment(1)
Thanks, I edited above according to your refactoring suggestion.Lowly
U
4

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

Urine answered 18/11, 2009 at 2:44 Comment(1)
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: #188662Lowly
G
3

I know this is not an answer to the question, but I will be able "understand" the code much better if there are tests, such as:

assert positions([1]) == [1]
assert positions([2, 1]) == [1, 2]
assert positions([2, 2, 1]) == [1, 1, 3]
assert positions([3, 2, 1]) == [1, 2, 3]
assert positions([2, 2, 2, 1]) == [1, 1, 1, 4]

That will tell me, one year from now, what the code is expected to do. Much better than any excellent version of the code I have seen here.

Am I really off topic?

The other thing is, I think "readability" depends on the context. It depends who will maintain the code. For example, in order to maintain the "functional" version of the Groovy code (however brilliant), it will take not only Groovy programmers, but functional Groovy programmers... The other, more relevant, example is: if a few lines of code make it easier to understand for "beginner" Clojure programmers, then the code will overall be more readable because it will be understood by a larger community: no need to have studied Clojure for three years to be able to grasp the code and make edits to it.

Grizelda answered 13/11, 2009 at 3:27 Comment(0)
D
3

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.

Dukedom answered 13/11, 2009 at 14:58 Comment(0)
C
3

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.

Chak answered 27/6, 2010 at 6:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.