Clojure Code Can Edit Clojure Code. Here’s Why That Matters.

Neon lines on wall

Clojure and Lisps can do something no other language can:

Edit their own code.

But JavaScript can create a string of JavaScript and eval() it.

And JavaScript functions can return functions.

So how is this different?

This isn’t just generating code.

Clojure can rewrite existing Clojure code.

This can make Clojure easier to understand and more terse.

Macros

Clojure edits its own code via macros.

You can define macros with defmacro.

And you invoke a macro like you invoke a function:

(<macro name> <forms>)Code language: HTML, XML (xml)

Here’s how to invoke the -> macro:

; V--- macro name
(-> response       ; <- forms
    (get "quotes") ; <-
    first          ; <-
    (get "quote")) ; <-Code language: Clojure (clojure)

Clojure compiles this macro to:

(get (first (get response "quotes")) "quote")Code language: Clojure (clojure)

How macros help

With a response from an HTTP endpoint, maybe you want to get the first quote:

(def response [
  {"quotes" [
    {"quote"  "The first quote" 
     "author" "José"}
    {"quote"  "The second quote" 
     "author" "Juan"}
   ]
   "total" 2
   "skip" 0
])
Code language: Clojure (clojure)

Here’s 1 way to do that:

(get (first (get response "quotes")) "quote")Code language: Clojure (clojure)

But that’s confusing, because it reads right to left.

It’d be easier to read top to bottom:

  • Start with HTTP response
  • Get "quotes" from that response
  • Get the first quote from that
  • Get the "quote" value from the quote map
Code language: Clojure (clojure)

With the -> macro, you can write this top to bottom:

(-> response       ;; Start with HTTP response
    (get "quotes") ;; Get "quotes" from that response
    first          ;; Get the first quote from that
    (get "quote")) ;; Get the "quote" value from the quote map
Code language: Clojure (clojure)

This inserts the result of the previous form as the first argument of a form.

For example, line 2 here:

(-> response
      (get "quotes")
      first
      (get "quote"))
Code language: Clojure (clojure)

Becomes this:

(-> (get response "quotes") ;; inserts response as the 1st argument
      first
      (get "quote"))
Code language: Clojure (clojure)

The whole macro compiles to:

(get (first (get response "quotes")) "quote")Code language: Clojure (clojure)
A pipeline of data is easier to understand than nested functions

But can’t you do that in JavaScript?

Not in a clear way.

Ideally, you could write this in JavaScript:

thread(
  response
  get("quotes"),
  first,
  get("quote")
)Code language: JavaScript (javascript)

…but that wouldn’t work.

Because on line 3 below, JavaScript can’t insert response as the first argument, to produce get(response, "quotes").

thread(
  response
  get("quotes"), // can't insert response as first argument
  first,
  get("quote")
)
Code language: JavaScript (javascript)

JavaScript code can’t edit JavaScript code.

Here’s what you could do in JavaScript:

function thread(x, ...forms) {
  return forms.reduce((acc, [fn, ...args]) => {
    return fn(acc, ...args)
  }, x)
}

thread(
  response
  [get, "quotes"],
  [first],
  [get "quote"]
)
Code language: JavaScript (javascript)

But the thread() call isn’t clear.

What is get on line 3 below?

thread(
  response
  [get, "quotes"],
  [first],
  [get "quote"]
)
Code language: JavaScript (javascript)

It’s a function, but it’s hard to see that.

And what’s "quotes" on line 3?

It’s an argument for get(), but there’s no way to tell without looking at the definition of thread().

Even this would be more clear than that JavaScript example:

get(first(get(response "quotes")) "quote")Code language: JavaScript (javascript)

In contrast, Clojure is clear:

(-> response
    (get "quotes")
    first
    (get "quote"))
Code language: Clojure (clojure)

On line 2 above, (get "quotes") looks like it gets quotes.

You don’t have to know anything about this macro to understand that.

And once you read about the -> macro, you see it inserts response as the first argument of get.

So when you get used to it…

Clojure can:

  • Have less code
  • Be easier to understand

But what if you write bad macros?

Then you’ll create even worse code than you could with plain functions.

Clojure lets you create very elegant or very dirty code.

This makes Clojure powerful

Clojure and Lisps can do something that no other language can.

So despite the risk of creating a mess with macros…

You can use them right.

And create an elegant project with little code.

Leave a Reply

Your email address will not be published. Required fields are marked *