The Problem with Lisp

Lisp is a very old programming language but has features, that seem modern and innovative to this day. Why then, was there no adoption to mainstream? The elephant in the room are its many parentheses.

In the Lisp community it seems to be a joke why so many people just don’t get how awesome and elegant Lisp is and instead are only complaining over the parentheses. Let’s see some examples.

(join "\n" (surround-each "<li>" "</li>" ("a", "list", "of", "items")))

Some people prefer having the function name before the parentheses

join("\n", surround_each("<li>", "</li>", ["a", "list", "of", "items"]))

But that’s not the big difference [1]. The bigger problem is that the flow of instructions is reverse to our reasoning. It would be better the other way round:

((("a", "list", "of", "items") surround-each "<li>" "</li>") join "\n")

This aligns better with our way of thinking as well as with the execution, starting from the data to how we transform and reshape it.

However, there is still a problem with the parentheses. Our minds have to deal with the whole transformation because everything is in context and this is shown by the parentheses. It would be much easier if we could close one thing and then deal only with the rest of the transformation (or even only with the next step).

["a", "list", "of", "items"].surround_each("<li>", "</li>").join("\n")

Of course, the parentheses have their advantages. For example, we don’t need to deal with all those commas and it is easier to deal with the code as data (one of the very things that make Lisp so awesome). But it still changes how approachable our code is in the first place.

Syntax and Code Structure: Two Sides of the Approachability Coin

Stopping Early

While syntax plays a crucial role in approachability, this principle applies not only to Syntax. Another aspect of coding that affects our understanding is the structure of our logic. This leads us to consider how the order of our code can impact its readability. I have seen a lot of code like this:

function(a, b, c) {
  var error = false;

  if (a == "great") {
    if (b && c) {
      return a + b + c;
    } else {
      error = true;
    }
  } else {
    error = true;
  }

  if (error) {
    console.log("something went wrong");
  }
}

This is hard to reason about. It would be much easier if we rewrite it to fail early. This makes it not only easier for the code to ignore the rest if some of the invariants do not hold, but also for us as the human readers.

function(a, b, c) {
  if (a != "great") {
    return console.log("something went wrong");
  }
  if (!(b && c)) {
    return console.log("something went wrong");
  }

  return a + b + c;
}

This is much easier to understand. But we often end up with the first code because it is easier for us to think about the happy path first.

The Syntax for method chaining above leads us to reason in that way. We could even support the example through Syntax.

function(a, b, c) {
  invariant a == "great" !> "something went wrong"
  invariant b && c !> "something went wrong"

  return a + b + c;
}

This lets us continue thinking the happy path first while still structuring the function in a much easier way.

As this example illustrates, code structure is influenced by the syntax. A language with a syntax like in the last example (and maybe no support for traditional if clauses) would lead the developer to structure the code in a more readable way.

Top Down Thinking

By stopping early, we’ve improved the readability of our conditional logic. Similarly, the overall organization of our codebase can significantly impact how easily others (and our future selves) can understand our work. This is particularly evident when considering the order in which we present our code.

Many languages force you to write down the code in the order of execution. This makes you think about less important things first. Consider this code:

import transformation_of_c;

function helper(a, b) {
  ...
}

function important_feature(a, c) {
  return helper(a, transformation_of_c(c));
}

This makes you read all the less important stuff before you get to the meat. While modern languages often change this in that you can define functions in every order, why do we still have to do the imports first? Why can’t we write something like this:

function important_feature(a, c) {
  return helper(a, transformation_of_c(c));
}

function helper(a, b) {
  ...
}

import transformation_of_c;

This lets us start with a high-level picture and we can decide to read on, depending on whether we need to understand the details or not.

Conclusion

To create great software we need to reduce the cognitive load. The less we have to think about the details the more we can focus on the really important stuff. While the ideas behind our code are more important than the syntax (and even the language we choose), a good syntax can reduce cognitive load and make it much easier to write good software.

We should experiment with different syntaxes for the same programs to find out how to make the intent of the code more obvious.


1. although it is what people are used to. And that’s a very important factor in making things understandable