Before you go, check out these stories!

0
Hackernoon logoClojureScript Macros: A (Not So) Long Journey [Part I] by@frozar

ClojureScript Macros: A (Not So) Long Journey [Part I]

Author profile picture

@frozarFabien ROZAR

Clojurist at heart

I’m learning the Clojure and ClojureScript craft and I must say, I enjoy it. This article explains what leads me on the track of learning Clojure/Script macro. This is the first part of my journey on this topic.

What’s the situation?

To understand a programming language's value, it's generally relevant to detail a context of application of this feature. So here is the situation.

Tiny study case

I’m working on a web application project using ClojureScript. The application draws a diagram which can be seen as a graph: I keep track of nodes and the links between them.

Nodes are rendered as bubbles (ellipses) and links as straight lines between bubbles. This information is stored in a Reagent atom, appstate. Reagent is a ClojureScript library which makes the binding to React, the famous JavaScript library.

I’m a beginner, and setting up unit tests for my project was not top first priority. In the beginning I didn’t know what development environment to use (and you have the choice with Clojure/Script), how to connect things together, how to translate my ideas into code, what is the standard way of doing this or that, and so on…

So I developed for a while without unit tests, I must confess.

But with enough experience and confidence, at some point I felt it was the time for me to put in place some unit test around functions touching the application state. The following snippet shows you its skeleton:

(ns bubble.state
  (:require [bubble.bubble :as bubble]
            [reagent.core :as reagent]))

(defn initial-application-state []
  {
   :bubbles [bubble/root-bubble]
   :links []
   })

(defonce appstate
  (reagent/atom (initial-application-state)))

The appstate atom is initialised by the initial-application-state function and it contains an hashmap:

  • the :bubbles field is associated with a vector of bubbles (which are hashmaps themselves)
  • the :links field with a vector of couple of bubble ids

Below the appstate initialisation, you have a bench of functions to Create/Read/Update/Delete it (not shown in snippet). So far so good.

By writing the first test, I realised that I didn’t like the way my functions were written. For example, let’s take a look at the add-bubble! function:

(defn add-bubble! [bubble]
  (swap! appstate update :bubbles conj bubble))

As you can see, this function is straightforward:

  1. I call the swap! function to modify the appstate atom;
  2. I want to update the contained hashmap;
  3. Access to the :bubbles field which contains a vector of bubbles;
  4. Finally append (with the conj function) the input argument bubble to this vector.

I like to write tiny functions, simple and straightforward, that do only one thing : sweet. But if you’re familiar with Functional Programming, maybe you noticed that add-bubble! uses the appstate variable in its body; this variable is reachable because it’s a global variable declare ahead of add-bubble!.

Which means that the pretty cute add-bubble! function, it’s not a pure function: oh my god!

On the other hand, it’s fairly common in web application to use (at least) one global variable to store the current state of the application. So is it that harmful to use not pure functions?

Before dealing with this question, if you’re not (yet) aware of what a pure function is, well let’s clarify this term.

Pure function: it’s not rocket science

Daniel Higginbotham, author of the pedagogic, funny, well written, amazing (I could say more but I'll stop there) book Clojure for the Brave and True, characterises pure functions as follows:

1. It always returns the same result if given the same arguments.
2. It can’t cause any side effects.

That’s it. This is the kind of function that you wrote when you learnt the basics of any programming language. Nota Bene the famous Hello World() — first example of function to write —  is not a pure function as it performs I/O instruction: writing outputs to terminal.

In our study case, add-bubble! is not a pure function as it reads/writes appstate global variable. Well, my function is not a pure one, so what? Is it serious, doctor?

I would say it depends on the purpose of your project. It’s not the same if you’re trying to make a proof of concept or if you’re adding a feature to a large code base.

In a short run, add-bubble! is a good candidat: it does the job, it’s readable, so nothing to declare. For me, the only downside is that this function cannot be tested elegantly. There are situations where test are not important: proofs of concept, technical investigations, projects which must be done in short slot of time, and so on.

But in a long run, the multiplication of side effect functions touching a global state tends to make the maintainability more difficult. As it’s simple and seems harmless to read/write to a global entity from anywhere, the different parts of the code tend to get tangled to each other. Some dependencies between functions may exist implicitly, but these links can be hard to highlight.

Globally, the project takes the direction of a big monolithic architecture. It becomes difficult to identify a piece of code which can be replace by a standard library which solves the same problem.

This also makes the debugging part difficult. At some point, it would become systematic to execute one’s whole application to inspect a global variable state before and after the execution of a given function. In a such situation, the code tends to become more and more “write-only” as one may say, which is not a good sign for its maintainability.

If you want to learn more about monolithic architecture, you can read Monolithic vs. Microservices Architecture.

Another downside of side effect functions: they generally do not compose with each other. Composing functions is the ability to use the output of a function as the input of another one. If you ever used a pipe in a *NIX terminal, this is a perfect example of program composition.

As Functional Programming is getting more and more attention, there are also a lot of articles here and there speaking about the benefits of using pure functions. These benefits are not restricted by any programming language; for example Ken Aguilar gives a concrete study case in Javascript.

I my opinion, this regain of interest about Functional Programming is mainly due to these 2 points: maintainability and ability to compose functions. If you want to lean more about this topic, I recommend Eric Normand, teaching Clojure through his website purelyfunctional.tv, whom gives his opinion in Why is functional programming gaining traction? Why now?

One thing I like the most about Functional Programming is that it allows me to evaluate the quality of a piece of code with more objective criteria. In this study case, the fact that add-bubble! is a short function and does one thing doesn't mean it is really convient in a long run.

The first signs of this is the ability to be tested and its composability in my humble opinion. From my personal experience, a written code last more than I expected, so I prefer to use concepts which make it easier to maintain. Except for a school/study project, how often did you finish a project and then trash the source code away?

And why did I want to use macro?

With the early thought about how to test add-bubble!, its side effect aspect began to bit me:

  1. The global appstate variable should be set and check respectively before and after the execution of add-bubble!. Compare to testing an output from a function, this way seems cumbersome;
  2. What’s happen if I run the tests in a background process and simultaneously develop the code with a REPL session? Would the tests execution modify the current state of the application?

I didn’t define what is a REPL session but quickly, REPL stands for Read-Eval-Print Loop. It is the way that Clojure enhances the workflow development experience. It allows you to work with a quick feedback loop between the written code and its result after execution.

If you want to learn more of this topic, the Clojure official documentation page Programming at the REPL: Introduction is really what you need to read.

Anyway I didn’t want to spend time over these puzzling, uninteresting issues: setup unit test should be a simple task. With pure functions, all these questions simply disappear. 

After a refactoring, I ended up with this:

(defn- add-bubble [appstate bubble]
  (update appstate :bubbles conj bubble))

(defn add-bubble! [bubble]
  (swap! appstate (fn [appstate_arg] (add-bubble appstate_arg bubble))))

One pure function add-bubble and one function with side effect add-bubble! which uses the pure version of itself to update the global application state appstate. It’s a common convention to put a “bang” (an exclamation mark) at the end of a function name if it does side effects in Clojure/Script world.

Instead of testing add-bubble! directly, I could now test the add-bubble function which takes the application state and other argument(s) as input, and return a new application state.

But with this solution, you would ask me:

  • "Will you write two functions for each one which modify the application state in order to be able to test them easily?"
  • I’m afraid to say: "Yes." 

This work is repetitive, error prone and boring, but the bang functions are necessary, easier to use in the other parts of the project.

Despite the fact that I decided to do it this way, there is a good news: bang functions follow a simple pattern in their construction. I would like to write a code which takes a pure function as its input and generates the side effect version associated with it…

Here we are: it can be done thanks to macros! Macros allow to generate code at compile time. The macro I need would take as input a pure function with an arbitrary arity (number of argument) and generated the side effect version of it.

This is how my journey in learning Clojure/Script macro begins. This little use case gives me enough motivation to jump over the gap.

Conclusion

“What? That’s it? You didn’t tell anything about macro! Refund! Refund!”
It’s not a trap, I swear. It’s my first article and I realised that the motivation part of the work (digressions involved) takes longer than what I thought. I finally preferred to split the story to get something more digest. However, I hope you learned something through this introduction.

The second part of this journey is coming soon.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!