Empty Promises and Other Heresy


How functional programming in Clojure made me a better person

An illustration of a clojure hot air balloon floating away from java-mountain with a happy man aboard.

My name is Toni and I’m a programmer

I wrote my first choose-your-own-adventure game when I was eight years old. I had learned to instruct my trusty C64 to wait for input, process the input, print output, rinse and repeat. My first university classes were basically the same, although the process was much slower because my Java code had to be compiled in each iteration. However, we soon ditched any notion of quick prototyping and moved on to Real Programming, which evidently involved lots of modelling, diagrams, classes, objects, and instances.

Now, having used Clojure for a year at work, I’m back to being that kid who enjoyed programming. I write functions that take data as input, do stuff with that data, and output data. That’s it. I can try out stuff in the REPL, look what I got out of it, and try something else. I see the data. I can touch the data. I get immediate feedback. It’s how I have always loved to work.

Reading Clojure code

I will give a few code examples in Clojure. They are brief (Clojure code always is), but you should know the basic syntax. If you already do, come play with us. If you don't, you could go back and forth to the Clojure cheatsheet, or even take a crash course in Clojure. However, I can tell you the basics in no time. You already know how functions work, right? If so, the following illustration summarizes Clojure syntax for the reader more accustomed to imperative languages:

Move opening parenthesis to front

Adapting your function calls to Clojure syntax

Now that you know how to read Clojure, let’s get started.

Stuck in the 50s

It used to be so that we cared where computers store their data. It made a lot of difference whether the OS loaded your stuff at 0xA5A79 or 0x77F4C (especially if you got this for Christmas). It made even more difference how and when and what memory was accessed or — gasp — manipulated.

Nowadays, most of us don’t think about the day-to-day issues of memory management. “How many bytes does it take to store my integer?” is no longer the right question. “Why should I care?” is a more relevant one. The following code snippets illustrate the difference between describing implementation details and the meaning of data.

class Person {
  private float height;
}
(s/def :person/height (s/double-in :min 0.0 :max 3.0))

Even this simple example shows that data is much more than byte counts. Nonetheless, our de-facto programming languages are still standing on the shoulders of Fortran and C, rooted close to hardware. We are more concerned about the “how” - the exact sequence of operations a computer must perform - than the “whats” and “whys” of programming.

Our focus on the lower levels of computing is in direct contrast to the needs of our customers, for whom declarative will always be greater than imperative. They want to predict when a stone crusher needs maintenance, or guide a customer to book travel that fits their needs and preferences. Beyond meeting the criteria of security and reliability, they don’t (and shouldn’t) really care about the implementation from a business perspective. They need results, not operations, dammit!

A tale of two camps

Programming languages have evolved from two major traditions: the bottom-up camp and the top-down camp. The bottom-up camp approached programming with the imperative style: Let’s start with the hardware-related operations and add abstractions, as long as they don’t sacrifice performance. The more academic top-down camp aimed for mathematical rigor: We’ll ignore performance (for now), and figure out instead, what computing is really about. The approach of the bottom-up camp was detailed control over processing using fine-grained operations, while top-down proponents aimed for simplicity by finding out a small set of powerful constructs.

The bottom-up approach led to object-oriented programming, which in turn led to many sleepless nights refactoring deep inheritance hierarchies and God Objects. Meanwhile, the top-down camp evolved into functional programming, which led to higher salaries and better job satisfaction [citation needed].

In spite of being separated at birth, these two traditions are now coming close to each other. What used to be the right thing, but terribly slow, is now practical and efficient. Even the most hipster of frontend developers are now embracing functional programming — thanks in large part to React, Flux, and their advocates.

However, there is a difference in working with a language designed for functional programming, compared to working with JavaScript. I recently needed to chain a series of functions, so that the next function would operate on the result of the previous one. Further, I needed to pass the same arguments to each function, as the number of tokens to translate depended on the language and its translations. In JavaScript, the solution became very convoluted as compared to its Clojure(Script) counterpart.

const translateRank = (juniorWoodchuck, translations, language) => {
  const rank = getRank(juniorWoodchuck);
  if (rank) {
    const tokens = getTokens(rank, translations, language);
    const translatedParts = translate(tokens, translations, language);
    return format(translatedParts);
  }
  return defaultTranslation(language);
}
(defn translate-rank [junior-woodchuck translations language]
  (if-let [rank (:rank junior-woodchuck)]
    (-> rank
        (get-tokens translations language)
        (translate translations language)
        format)
    (default-translation language)))

Maybe we could live without a construct for chaining functions like this, or use objects and method chaining (shudder!). However, such deficiencies stack up and encourage a certain style of programming, i.e., not a functional style. For example, the lack of proper function chaining makes lambda lifting quite unattractive.

The hermetic encapsulation

People are celebrating Kotlin for inventing the assignment operation to replace the setters of Java. While it’s certainly better than Java, and Kotlin is embraced by many developers at Vincit, I forget: Why did we need the OO encapsulation in the first place?

The answer is of course: “Someone might do bad things to my data, so I must protect it!” Well, there is a better way. In Clojure, all data is immutable. Once you have it, you can’t change it. I can lay all my data bare in the open, and you won’t be able to mess it up.

I had a really hard time accepting immutability. I had experience of many imperative and object-oriented languages, but the notion of immutability was nonetheless completely foreign to me. Sadly, I have seen evidence that this confusion is a common one. Prime examples are JavaScript libraries that wrap their Immutable.js state inside a singleton object, along with getters and setters. Why would you ever do that? All I wanted was to access your data, and now you’ve created the North Korea of JavaScript libraries.

The empty promise

Here’s a brief summary of React: Components take input as “props” and output beautiful HTML UIs. When props change, the component re-renders — reactively, you might say. So, when I tried a new visual component library for React, I expected to throw data at the library, and get it rendered. What I got instead, was an empty promise.

The library wanted me to use a promise to signal when my data is ready to be rendered. This was not a good fit, since I was using Redux for my state management and redux-saga for handling all my side-effects (i.e., things that are not pure; in this case, interact with the unclean outside world). I was forced to write some ugly code that resolved an empty promise (yuck!) after executing the side effects.

Not a big deal, you say? Well, it’s difficult to look the other way, once you’ve seen the light. I’m now used to just managing the data of my app, and getting beautiful reactive UIs by default. The following code example from my re-frame frontend app shows how I update a list of items in response to a :shuffle event:

(re-frame/reg-event-db
  :shuffle
  (fn [db _]
    (update db :ids shuffle)))

When my handler changes the in-memory database (a good metaphor for any frontend project, by the way), the views are automatically updated. The changes propagate all the way to the underlying React components, which handle the re-rendering efficiently. No need to explicitly request a re-render, or other ominous trickery.

Concussions

My first encounter with functional programming felt like a minor head trauma. Trying to read Clojure’s lispy syntax caused my brain to short circuit. However, as the syntax of the language itself promotes a logical structure for Clojure programs, it quickly became understandable. After overcoming the initial parenthesis paralysis, I dared to write a few lines of Clojure myself. Soon, I was able to read Clojure, and even spot some bugs in other people’s code, when they were clear and obvious.

Not only did I learn to type Clojure, but being exposed to functional programming changed the way I write other code as well. Most of my JavaScript functions are now 1 to 5 lines long. Anything over that length is suspect of complecting things. I’m also less prone to introduce local state or free variables, which always lead to a mess, while preferring small, reusable, and testable pure functions.

This is how Clojure made me a happier and better programmer. I dare you to challenge your old assumptions and give it a go. There is no need to settle for zero being equal to object plus empty array, and no need to glue on immutable data as a second-class citizen of your codebase. Learn a new language instead, and be happy!

Toni Vanhala

Toni Vanhala
Toni programs in Clojure for fun and profit. He has Ph.D.

3 kommenttia

Thanks for the post! A couple of points. Your height spec indicates the value :person/height will be between 0 and 3. It says nothing about metres.

Toni Vanhala sanoo:

True. The spec does not explicitly state that the height is in metres. If you’re concerned about people making an error because of it, you could either A) make a more explicit name (e.g., height-in-metres), or B) make height a composite value with unit explicitly specified.

If your program is going to deal with many units, then I would prefer option B. In this case, you could write a multi-spec to enforce a different spec for SI units, Imperial units, US units, and so on. You can read more about multi-spec here: https://clojure.org/guides/spec#_multi_spec

Liity keskusteluun