Keith's Notes

Thoughts on Clojure (after a week)

Apr 16, 2023, 3:49 PM

So last week I started learning about Clojure. I'm starting to get a taste for it and thought I'd share some very early reactions.

First, it's DIFFERENT. The syntax is just really different. Pretty much everything is enclosed in parentheses. All functions are, and everything is a function, so... yeah.

In most languages, you'd do something like this:

println(value1, value2)

In Clojure, it's

(println value1 value2)

If you needed to compute one of the values that you pass into the function, you'd possibly create a variable for that.

int value1 = 2 + 3
int value2 = 5 + 6
println(value1, value2)

In Clojure, it seems like its preferable to inline this kind of thing. Also, what you'd think of as operators are also actually functions, so do add two numbers, it's

(+ 2 3)

So the above becomes

(println (+ 2 3) (+ 5 6))

You could create interim variables usind def:

(def value1 (+ 2 3))
(def value2 (+ 5 6))
(println value1 value2)

But the compiler does give you a warning for this inline def so I think it's frowned upon.

There's another way to create interim variables using the let function, which I'll describe below. But it's a bit more complex.

Even function definitions are actually functions, so in another language, you might have

func addTwoNumbers(a, b) {
  return a + b
}

Now you'd do:

(defn add-two-numbers
  [a b]
  (+ a b))

Silly example, but you get the idea. But even that example raises a few more points.

  1. Most languages go for camelCase or snake_case. Clojure is one of the few I've seen that favors kebab-case for identifiers. My first reaction was that it would break everything because it would try to subract case from kebab. But subtraction would be done with a function call (- something something-else) so there is no conflict.

  2. The variables in square brackets represent the arguments to the function. Didn't see that coming.

  3. Clojure style seems to like stacking up the matching parentheses on the end of the last line of a block. I would have done this like

(defn add-two-numbers
  [a b]
  (+ a b)
)

But that doesn't seem to be the preference. Here's the last line of a function I wrote recently.

...
               result)))))

I'd be dead if my editor didn't highlight matching braces.

  1. Where my commas at? Apparently they are optional, but none of the code I'm seeing is taking that option.

Of course most of these are stylistic choices, but when in Rome...

But let's go back to the brackets. They can be used in several different ways. The simplest is just a vector of values:

[1 2 3 4]

The other one I just showed is to get the parameters of a function.

The most confusing one is in binding values to identifiers. So far I'm mostly seeing this in let statements.

(let [a "foo" b [] c 7]
  (println a b c))

This one was the toughest for me to read at first. I was like, "OK, we have vector of six values..." but that doesn't make any sense. In fact, the let function creates a closure, lets you create some local variables bound to other values, and use them within that closure. So this is really saying:

Some code lists the bindings out in a single line like that. More readable code uses some line breaks in lieu of commas:

(let [a "foo"
      b []
      c 7]
  (println a b c))

You can also do destructuring with square brackets. One place you'd see this is in function parameters. Say you have a function that gets passed a collection of some kind. You might want to do something with the first two elements of that collection. You could do:

(defn foo
  [[a b]]
  (println "the first two elements are" a b))

Here, the outer brackets signify the function param(s) and the inner ones do the destructuring.

Or you might want to get the first element and do something with the rest of the elements:

(defn foo
  [[a & everything-else]]
  (println "the first element is" a)
  (println "all the other stuff is" everything-else))

Powerful, but it takes some practice to be able to actually think with it all.

Anyway...

OK, so that's all just a rant about the differences. But it's fine. I chose Clojure because I knew it would be very different and I'd have to change my thinking about how to code things. It's certainly having that effect, which is great.

I understand that Clojure is a Lisp, i.e. one of the languages based on Lisp. So a whole lot of these differences are probably very similar in any of those languages.

Finally, one thing that really stuck out in my mind was how easy a language like this must be for an interpreter or compiler to parse. In examples like this, you can pretty much see the abstract syntax tree right there in the source code:

(println (+ 2 3) (+ 5 6))

Anyway, that's just some early thoughts. I'm continuing to bang away and will probably have more ramblings later.