to all posts

Being very careful isn't a valid strategy

Published

Here’s a snippet of JavaScript:

function decrementHealth(player) {
  player.health -= 1;
}

Here’s how I’d write the same thing in Elixir:

def decrement_health(player) do
  Map.update!(player, :health, &(&1 - 1))
end

And for good measure, and since I’ve just started picking it up, how about some Clojure:

(defn decrement-health [player]
  (update player :health dec))

The point of these examples isn’t to showcase the differences in syntax (even though it is hard to argue against the elegance of a LISP). The point is what happens at the call site: The JS version modifies the original player object whereas the other versions don’t. That is, in JavaScript you’ll see

let player = { health: 10 };
decrementHealth(player);
console.log(player); // { health: 9 }

With the other languages, the original player value is unchanged:

user=> (def player {:health 10})
#'user/player
user=> (decrement-health player)
{:health 9}
user=> player
{:health 10}

In contrast to the JS function, they are both pure, which makes them simpler to test, easier to reason about, and means you can confidently call them without worrying about messing with your program state in unintended ways.

Now, it is possible to write a pure JavaScript function which does the same:

function decrementHealth(player) {
  return {
    ...player,
    health: player.health - 1,
  }
}

But what if instead of health being a number, it’s an object containing other attributes as well? Something like

{
  health: {
    current: 8,
    max: 10,
  }
}

Well, then we’d have to do

function decrementHealth(player) {
  return {
    ...player,
    health: {
      ...player.health,
      current: player.health.current - 1,
    }
  }
}

But this is still messy when compared to

def decrement_health(player) do
  put_in(player, [:health, :current], player.health.current - 1)
end

or

(defn decrement-health [player]
  (update-in player [:health :current] dec))

and still, the point isn’t really the syntax.

The point is that state modification should be intentional and obvious, and certain tools force your hand in making it so. If I want to have a state object that changes over time, I’ll have to use a GenServer in Elixir, or an atom in Clojure, or stick the value in a database—all clear signposts that this is a piece of state which will change with time. But using a map, the most obvious data structure to group and label related data, isn’t, and it shouldn’t be.

And whilst it is possible to write code that makes the same guarantees with Javscript, or Python, or whatever mutable language with functional capabilities you find yourself using, doing so is predicated on a strategy of being very careful. It’s the programming equivalent of pulling out as a birth control strategy: sooner or later it will fail, and you will have to face the consequences.

At the very least, put on a condom.