Make the Impermissible Unrepresentable

Image of Author
April 21, 2022 (last updated September 21, 2022)

Or, Make Illegal States Unrepresentable

Or, Make Impossible States Impossible

Logical Possibility as "Comprehensibility"

Speaking very loosely, a computer is only constrained by logic. This is a "comprehensibility constraint" (still speaking loosely). Humans are also constrained by logic, but also by many other things, like the laws of physics. For example, defying gravity is comprehensible, that is, logically possible, but physically impossible.1

Software Use-Case as "Use-Case Permissibility"

A human-level constraint is use-cases. You write software to perform actions. All the other logically possible actions are out of scope. Not permissible. There is a wide gap between what is logically permissible and what is use-case permissible. It is in this gap that many types of bugs appear.

Many types of bugs are attributable to the fact that your code expressed and/or represented an impermissible state of affairs insofar as your desired use-cases are concerned. Speaking very loosely, it could be argued that any bug fix is an attempt to make the impermissible state unrepresentable.

Pebble Pizzas are Impermissible

Let's consider baking a pizza. A logically possible pizza is a pizza with pebbles for toppings. (This is also a physically possible pizza.) However, a pebble pizza is use-case impermissible because nobody wants to eat it.2

function bakePizza(toppings) {
  return new Pizza(toppings);
}

bakePizza("pebbles");

Let's fix this bug by making pebble pizzas unrepresentable.

function bakePizza(toppings) {
  if (toppings == "pebbles") {
    throw new Error("No!");
  } else {
    return new Pizza(toppings);
  }
}

bakePizza("pebbles");

The above code snippet can not represent a pebble pizza. You can still manually type new Pizza('pebbles') and thereby represent a pebble pizza. But, the point is to make it unrepresentable in your workflows.3

I will not delve too deeply into the perpetual refactoring journey that is the art of making the impermissible unrepresentable. But, that being said, one of the more common techniques that is used in this regard is constraining parameters. Let's see what that would look like.

const possibleToppings = ["pepperoni", "cheese", "mushroom"];

function bakePizza(toppings) {
  if (!possibleToppings.include(toppings)) {
    throw new Error("No!");
  } else {
    return new Pizza(toppings);
  }
}

In typed languages this can look like an enum parameter

enum Toppings {
  Pepperoni,
  Cheese,
  Mushroom,
}

function bakePizza(toppings: Toppings) {
  return new Pizza(toppings);
}

In functional programming languages guard clauses can address this issue

def bakePizza(toppings) when toppings in ["pepperoni", "cheese", "mushroom"] do
  %Pizza{toppings: toppings}
end

def bakePizza(_), do: {:error, "No!"}

as can multimethods

def bakePizza("pepperoni"), do: %Pizza{toppings: "pepperoni"}
def bakePizza("cheese"), do: %Pizza{toppings: "cheese"}
def bakePizza("mushroom"), do: %Pizza{toppings: "mushroom"}
def bakePizza(_), do: {:error, "No!"}

Resources

Footnotes

  1. You could say it's "physically incomprehensible", but that feels like an odd phrase. I prefer the phrasing of: it's physically impermissible.

  2. Another phrasing could be "use-case impossible" but that sounds weird to me, while "physically permissible" and "logically permissible" both sound weird too. E.g., defying gravity isn't physically impermissible, it's physically impossible. Basically, I acknowledge I'm being imprecise, and saying what "feels right".

  3. Some notion of "truly unrepresentable" seems, on face value, a chimera. Any impermissibility that is not also incomprehensible will always be representable in software.