Power of Functions as an Explicit Statement

bit-cloud-wKhsnBy1DXc-unsplash.jpg

Photo by Bit Cloud on Unsplash.

Aka using functions to abstract information.

I'll be using javascript to describe it, but it could be any language with respective changes.

Functions/procedures are key to any programming language, low to the high level, no matter how close to the machine language, their importance is known. While this statement is ubiquitous, I would like to talk about something that isn't, a thing most people don't acknowledge.

Functions are mostly seen as a way to reduce code duplication and it is true. But they also provide a name for a block of code, abstracting it away. This, combined with their usage to adhere to DRY, can and will reduce bugs to the amount you can't believe.

We, human beings are good at executing repetitive things and have a special ability to do it, our brain can adapt to a task quickly and have an easy time repeating it. But this is sometimes not a good thing, because while we are in that comfortable, "doing things without thinking" mode, we do not calculate everything, so we are more prone to errors. So, little distractions can result in a butterfly effect that can cause hours worth of bugfixes or a car accident that can threaten your life. There are reports about people crash more often when they are in their comfort zone or near their homes.

So, abstraction helps to avoid these issues. While one can't abstract away driving totally (yet self-driving cars are quite good), can totally abstract away the code. So, let's start with an example, imagine having a game and a character with health points to track on. This can be represented with a game object which also will include a boolean that describes if our game is over. I won't use it though to reduce the complexity for the sake of the meaning, it is just there to be a filler.

const game = {
  over: false,
  player: {
    health: 20
  }
};

A function to check the logic is also needed, named tick.

// snip
function tick() {
  if (game.player.health === 0) {
    console.log("Your character has died. Game over.");
  }
  console.log(`Your current health is ${game.player.health}.`);
}

A possible way to kill the player is needed, so the tick function will also inflict damage to the character.

// snip
function tick() {
  game.player.health = game.player.health - 10;
  // snip
}

So, when the tick function is run twice, the player character will be dead. But, the code has some issues, if the health drops below zero, the character will never die. So, checking it is required.

// snip
function tick() {
  // snip
  if (game.player.health < 0) {
    game.player.health = 0;
  }
  // snip
}

As more features like this are implemented over time, the code gets bloated and wrong assumptions are made. For the sake of simplicity, we have to imagine the current code as if it were a complex block of code, spanning several thousands of lines. If that is clear, now we can track our assumptions. Let's look at the code in the combined form first.

const game = {
  over: false,
  player: {
    health: 20
  }
};

function tick() {
  game.player.health = game.player.health - 10;
  if (game.player.health < 0) {
    game.player.health = 0;
  }
  if (game.player.health === 0) {
    console.log("Your character has died. Game over.");
  }
  console.log(`Your current health is ${game.player.health}.`);
}

Starting with simple, health being zero means death is an assumption. While this is logical, and true in our case, it might've not. A game could have 0 as wounded and -1 as dead or another logic. So someone unfamiliar with the code, or even the person who wrote it can make mistakes while changing it. A mistake isn't needed, trying to update code, for example, changing the meaning of death to wounded and implementing -1 as death also requires a lot of hard work. So, abstracting away values can result in better readability and maintainability.

const HEALTH_DEAD = 0;
// snip
function tick() {
  // snip
  if (game.player.health < HEALTH_DEAD) {
    game.player.health = HEALTH_DEAD;
  }
  if (game.player.health === HEALTH_DEAD) {
    console.log("Your character has died. Game over.");
  }
  // snip
}

While this isn't a function, it reduces the source of truth to one value, so if code has bugs or needs to be updated, this helps a lot. These kinds of values that inserted into code called magic numbers and considered bad. We stated it as a constant variable so it isn't a magic value anymore. What we did to solve it is called indirection as it is an abstraction to describe something indirectly. While this is a form of indirection, it isn't the whole idea. So, the word indirection is bigger than this context. As we said context, let's return to our context. Let's tidy up the tick function and start with the isDead function. Oh, we don't have one, so what about if we create one?

// snip
function isDead() {
  return game.player.health === HEALTH_DEAD;
}

function tick() {
  // snip
  if (isDead()) {
    console.log("Your character has died. Game over.");
  }
  // snip
}

While I can remove the need for game.player.health < HEALTH_DEAD by checking for "<" instead of "===" in isDead, I won't for the sake of our example. You can imagine it as if it is needed. Now, reading the code is not just easier, also more meaningful. Our brain can figure out what's going on easily because we use the power of the naming to abstract away the implementation details. We don't need to think about the game object, player object and health and it being less than HEALTH_DEAD. It is buried within the isDead function (no pun intended) and by looking at the name of it, we can understand what's going on faster than before. It also creates the possibility of using the function in other places, reducing code duplication.

Now, there is another wrong assumption that code style forces on. The tick function corrects health if it is less than 0. Imagine the need for inflicting damage doesn't kill, instead injure. So, that means packing that with damage value.

// snip
function damage(amount) {
  if (game.player.health <= 0) {
    return;
  }
  let result = game.player.health - amount;
  if (result < 0) {
    result = 0;
  }
  game.player.health = result;
}
function tick() {
  damage(10);
  if (isDead()) {
    console.log("Your character has died. Game over.");
  }
  console.log(`Your current health is ${game.player.health}.`);
}

Now, this is better for our use case. Damage code and the code that corrects the health is extracted into another function, allowing us the flexibility of deciding when to correct and when not. Now, a way of killing a player can be implemented by setting health to -1, but that is implicit. We need to describe it explicitly, so we again use our functions to abstract away the implementation detail.

// snip
function kill() {
  game.player.health = HEALTH_DEAD;
}
// snip

If we were to run the code, we would see a bug. It is quite expected to have bugs, the main problem is not having them, rather solving them quick enough. While we implemented the injured logic, we forgot to change our dead requirement to -1 from 0. Luckily for us, we don't have to find all the usages and change them manually. We can simply change HEALTH_DEAD and be okay with it.

const HEALTH_DEAD = -1;
// snip

Now, our simple code is properly abstracted, can be understood clearly just by looking at the tick function. If we ever need to explore it further, we can check respective functions and see how it is implemented. This, also allows us to test our code without actually testing it. As we split our code into granular parts, it is easier to create logic without causing anything unwanted. We can generalize the code further if we ever want to and reduce the code duplication while expanding our abstractions.

const HEALTH_DEAD = 0;

const game = initialState();

function initialState() {
  return {
    over: false,
    player: createEntity("player", 20)
  };
}
function createEntity(name, health) {
  return {name, health};
}

function isDead(entity) {
  return entity.health === HEALTH_DEAD;
}
function damage(entity, amount) {
  if (entity.health <= 0) {
    return;
  }
  let result = entity.health - amount;
  if (result < 0) {
    result = 0;
  }
  entity.health = result;
}
function kill(entity) {
  entity.health = HEALTH_DEAD;
}

function tick() {
  damage(game.player, 10);
  if (isDead(game.player)) {
    console.log("Your character has died. Game over.");
  }
  console.log(`Your current health is ${game.player.health}.`);
}

I abstracted the player, added a function to create objects named entity that have name and health parameters, added another function to initialize the game state, altered other functions to work with an entity. Now, it is more generalized and we can reset our state to the initial if we ever want to. The thing I did is actually what classes do under the hood. This allows us to do this if we want to.

function createSwarm(amount) {
  const swarm = [];
  for (let i = 0; i < amount; i++) {
    swarm.push(createEntity(`swarm_${i}`, 5));
  }
  return swarm;
}
function nuke(entities) {
  entities.forEach(kill);
}
function cleave(entities) {
  entities.forEach((x) => damage(x, 10));
}
const swarm = createSwarm(50);
nuke(swarm);
cleave(swarm);

As you can see, we can share the same code and reasoning between different parts of the code. While this helps us to reduce the lines of code we duplicate, I value the reasoning part more. We generalized the concept of death in our code, managed to share that concept with all the code, reduced our mental workload quite a lot.

Here is the code is written if you need it for reference: https://codesandbox.io/s/power-of-functions-as-an-explicit-statement-tnh5w?file=/src/index.js

This way of coding has its performance issues with interpreted or just in time compiled languages like javascript, but as a rule of thumb, don't tackle them if you don't have to. Modern processors and the just in time compilers capable of handling most of it without you ever knowing it existed. So, unless required to do anything else, making it readable must be the topmost priority of any developer.

This was a long post, so, if you read this far, thanks a lot. While this is basic, most of us don't figure this, especially when we first start out. So, I would like to hear your thoughts about it and if it is helpful or not!

H2
H3
H4
3 columns
2 columns
1 column
5 Comments
Ecency