DEV Community

Cover image for Two Little Libraries
tom
tom

Posted on • Originally published at tgvashworth.com

Two Little Libraries

Hey.

I want to tell you about two little JavaScript libraries I built.

if-expression and try-expression do similar things, putting a bit of functional beauty around JavaScript's if and try statements.

They help you stick to using const over let and simplify refactoring.

Let's take a look.


In a minute I'll introduce the libraries separately and in detail, but to start with here's a full before/after example. First, some code you might write without these tools...

function doMagicalThings({ hocus, pocus } = {}) {
  let spell;
  try {
    spell = invokeTheDarkOnes(hocus);
  } catch (portalToTheUnderworldException) {
    spell = abracadabraAlacazam(pocus);
  }

  try {
    return castToString(spell); // See what I did there?
  } catch (unintendedConsequences) {
    return getErrorMessage(unintendedConsequences);
  } finally {
    cleanupOpenPortals();
  }
}

function getErrorMessage(error) {
  let errorMessage;
  if (error.code === 0) {
    errorMessage = "The giant spiders escaped.";
  } else if (error.code === 10) {
    errorMessage = "I dunno but I think kittens were involved.";
  } else {
    errorMessage = "Yikes. Run away?"
  }
  return errorMessage;
}
Enter fullscreen mode Exit fullscreen mode

... and the same thing with if-expression and try-expression:

import iff from "if-expression";
import tryy from "try-expression";

function doMagicalThings({ hocus, pocus } = {}) {
  const spell = tryy(
    () => invokeTheDarkOnes(hocus),
    (portalToTheUnderworldException) =>
      abracadabraAlacazam(pocus)
  );

  return tryy(
    () => castToString(spell),
    (unintendedConsequences) =>
      getErrorMessage(unintendedConsequences),
    () => cleanupOpenPortals()
  )
}

function getErrorMessage(error) {
  return iff(
    error.code === 0,
    () => "The giant spiders escaped.",

    error.code === 10,
    () => "I dunno but I think kittens were involved.",

    () => "Yikes. Run away?"
  );
}
Enter fullscreen mode Exit fullscreen mode

Major differences:

  • iff and tryy always return values
  • the clauses are (arrow) functions
  • there's no need to create a mutable binding (let, var) because you can return a value into a const

Read on for an in-depth look.


if-expression

$ yarn add if-expression
Enter fullscreen mode Exit fullscreen mode

if-expressioniff for short — is pretty simple to use. You can find the code on GitHub, and here's a rapid overview.

const largerNum = iff(
  a > b,
  () => a,
  () => b
);
Enter fullscreen mode Exit fullscreen mode

The first argument is the condition. If it evaluates to something truthy then the second argument — the first clause — is called. I've used arrow functions above for readability, but you can just pass a function:

return iff(
  featureFlag("fancy_new_thing"),
  useFancyNewThing,
  useUglyOldThing
);
Enter fullscreen mode Exit fullscreen mode

If the condition is false-y, the last argument — the else clause — is run.

It's variadic, so it supports a variable number of arguments, allowing you to supply multiple conditions and clauses. The conditions and clauses are paired up like if-else:

return iff(
  x < 0,
  () => "negative",

  x > 0,
  () => "positive",

  () => "zero"
);
Enter fullscreen mode Exit fullscreen mode

The last argument is always an else clause.

In any of the clause positions you can just supply a value if you want:

return iff(
  x < 0,
  "negative",

  x > 0,
  "positive",

  "zero"
);
Enter fullscreen mode Exit fullscreen mode

Thunk it up: A note about laziness, and functions as conditions...

In regular JavaScript execution, the conditions of if-else branches are lazily evaluated, that is, they are only run if they need to check for truthyness.

However, because if-expression is a plain ol' JavaScript function, the conditions are greedily evaluated: all conditions will be evaluated before if-expression has had a chance to decide if the first condition is truthy.

What does this mean in practice?

For most cases, it doesn't matter: you shouldn't be putting side-effecting code in if clauses, and the performance implications are negligible.

However, if the laziness matters to you, then pass the condition as a function:

return iff(
  () => x < 0,
  () => "negative",
  () => "not negative"
);
Enter fullscreen mode Exit fullscreen mode

However, that means functions cannot be used as conditions without explicit conversion to a boolean value, which is different from JavaScript's built-in if. In the following example, a.someMethod will be called:

return if(
  a.someMethod,
  doAThing,
  doAnotherThing
);
Enter fullscreen mode Exit fullscreen mode

To avoid this, you have two options. Either explicitly cast to a boolean...

Boolean(a.someMethod)
Enter fullscreen mode Exit fullscreen mode

... or return the method from a wrapper function:

() => a.someMethod
Enter fullscreen mode Exit fullscreen mode

try-expression

$ yarn add try-expression
Enter fullscreen mode Exit fullscreen mode

try-expressiontryy for short — is a whole lot like if-expression, but makes it easy to create try-catch-finally expressions. Again, there's code on GitHub.

Run some code and catch any errors, like you would use try-catch:

return tryy(
  () => doRiskyThing(),
  error => {
    logError(error);
    return 'Sorry!';
  }
);
Enter fullscreen mode Exit fullscreen mode

The first argument is always a function — a try clause. If it throws, the second argument — the catch clause — is used.

In the example above, if doRiskyThing throws, this code will return 'Sorry!'.

As you can see, the catch clause is passed the error that was thrown within the try clause.

Like if-expression, it's possible to just supply a value if there's an error:

return tryy(
  () => throwSomething(),
  { squibbles: 4 }
);
Enter fullscreen mode Exit fullscreen mode

tryy also supports a finally clause for cleaning up, as in try-catch-finally:

const result = tryy(
  ()  => ['Success', readFile()],
  err => ['Failure', err],
  ()  => closeFile()
);
Enter fullscreen mode Exit fullscreen mode

Note that, to avoid confusing JavaScript behaviour, anything you return from the finally function is discarded.


Here are some nice things you can do with these libraries...

This function is half finished, but the intent is clear: we're going to choose from the menu. To make that obvious, I've used only an else clause which will always run.

function chooseSomeLunch(person, menu) {
  return if(
    () => "not sure yet"
  );
}
Enter fullscreen mode Exit fullscreen mode

When we come to extend this code, the change is tiny:

function chooseSomeLunch(person, menu) {
  return if(
    onADiet(person),
    () => menu.salad,

    () => "not sure yet"
  );
}
Enter fullscreen mode Exit fullscreen mode

In this next example, the first clause is getting a little lengthy:

function shouldIEatThisCake() {
  return iff(
    nobodyIsWatching,
    () => {
      const isItLunchTime = consultTheAstrolabe();
      const caloriesBurned = activities.map(getCalories).reduce(add);
      // ... and so on and so forth ...
      return theFinalDecision;
    },

    () => false
  );
}
Enter fullscreen mode Exit fullscreen mode

It's easily refactored to be shorter and more readable:

function shouldIEatThisCake() {
  return iff(
    nobodyIsWatching,
    () => thinkRealHard(),

    () => false
  );
}
Enter fullscreen mode Exit fullscreen mode

Or even:

function shouldIEatThisCake() {
  return iff(
    nobodyIsWatching,
    thinkRealHard,
    false
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, we can easily build a "getter" function that tries to reach into objects, but will return a default value if the value doesn't exist:

function getIn(getter, defaultValue, data) {
  return tryy(
    () => getter(data),
    () => defaultValue
  );
}

const data = {
  a: {
    b: {
      c: "c"
    }
  }
};

const c = getIn(data => data.a.b.c, "default c", data);
const r = getIn(data => data.p.q.r, "default r", data);
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
leob profile image
leob

Pretty cool, shows how flexible JS is, you can really use the functional paradigm with it even when it wasn't designed for that.

Collapse
 
renannobile profile image
Renan Lourençoni Nobile

Really interesting idea, we had something similar at my last job, but we called it off.

Keep up the good work.