Posts

About

Expressive Design

April 04, 2020

Introduction

Recently I reread Clean Code by Robert Martin. Chapter 12, written by Jeff Langr, is all about Emergent Design. Emergent Design follows Kent Beck’s four rules from Simple Design. One of the rules is all about how well code expresses its intentions.

The clearer the author can make the code, the less time others will have to spend understanding it. This will reduce defects and shrink the cost of maintenance.

At the time I was rereading this chapter, I was coming off of working in the Dart programming language for the past 3 months. I was leaving the Dart world to do work on another project using JavaScript. This got me thinking about how programming launguages affect expressing intentions of code.

It had been a couple of months since I had worked in this JavaScript project. Going back in, nothing seemed to be as clear as it was were before. I looked at a function and all it took in was a variable called params. What the heck were params? I had no way of knowing what this was supposed to be without looking at the call sites for the function. Sometimes this didn’t help and I had to look at the call sites of the call sites or parse through some obscure test setup. In Dart this parameter would have had a type that I could easily look at and see exactly what it was. Not only that but if I was using an IDE, then I could do use autocomplete to see everything that params included.

I think that static types go a long way to help express the intent of the programmer. This got me thinking, what other aspects of programming languages help with intent? Two of my favorite features that I look for in languages are non-nullable types and abstract data types (ADTs). Both just happen to help express the intent of the programmer too. Unfortunately Dart has neither of these yet…

Non-Nullable Types

Non-nullable types take static types one step further. They tell someone using your code that this thing is never going to be null or this thing can be null. Having this distinction is very powerful. Now someone knows exactly where they need to handle null without having to refer to a schema or documentation elsewhere. Not only that, but they have to handle the nullability or the code will not compile. When something does change from non-nullable to nullable, the compiler will now catch every place that needs to handle this change.

Non-nullable types are a huge step in expressiveness and reducing defects. No more trying to remember if this thing could or could not be null. It is baked into the type system of the language which a compiler can check for you.

Algebraic Data Types

ADTs are a set of possible values for a certain type. A great example of this fetching data. This request can be in progress, successful with data, or have an error. Without ADTs, one might express with this with a class that has a boolean for in progress, a nullable data field, and a nullable error field. This is less than ideal because there are the possibility for impossible states. For example, what are you supposed to do when in progress is true but there is also data? This should never happen. This is where ADTs come in handy. Instead there would be a value for each possible state and the request could be in one. This gets rid of impossible states and allows for you to write some great intentional revealing code like the following:

interface Loading {
  kind: "loading"
}

interface Success<T> {
  kind: "success"
  data: T
}

interface Error {
  kind: "error"
  error: String
}

type Response<T> = Loading | Success<T> | Error

function displayResonse(response: Response) {
  switch (response.kind) {
    case "loading":
      return "Loading..."
    case "success":
      return `Successful response: ${JSON.stringify(response.data)}`
    case "error":
      return `Error response: ${response.error}`
  }
}

Another win with ADTs is for maintenance. Someone can add a value, remove a value, or change part of a value; and a compiler will catch any place that need to be updated.

ADTs are a great step in the right direction for expressing intentions. An ADT can be any one of the types defined but never anything else and never an impossible state.

Conclusion

A lot of things can contribute to expressive code including good names, small classes, small functions, and easy to read tests. I think that specific language features like static types, non-nullable types, and support for ADTs can take things one step further and improve expressing intentions in code.


Written by Jacob Oakes
I am a software architect who enjoys learning new things, clean code, and automated tests.