Sunday, April 07, 2013

Javascript Conditionals

As we all know, Javascript is a very flexible language. In the article I will show different ways to execute conditional code by using some common idioms from Javascript and general object-oriented techniques.

Default values

Javascript does not support default values for arguments and it is common to use an if statement or a conditional expression to set default values.

function swim(direction, speed, technique) {
  // Default value with if statement
  if (!direction) direction = 'downstream';

  // Default value with conditional operator
  var speedInMph = speed ? speed : 2;
}

I usually prefer to use an or-expression instead. The short-circuiting or, ||, avoids the repetition of the conditional operator and is, in my opinion, more readable. Another advantage of avoiding repetition is that a slow executing function condition such as fastestSwimmer() will avoid the performance penalty of calling the function twice.

  // Default value with or.
  var swimTechnique = technique || 'crawl';

  // Function is only invoked once
  var siwmmer = fastestSwimmer() || 'Michael Phelps';
}

Naturally, this technique is not limited to default arguments, it can be used to set default values from object literals too.

options = { kind: 'Mountain Tapir', }
var kind = options['kind'] || 'Baird Tapir';

A simple, yet useful, technique.

Update 2013-04-13, as Jeffery mentions in a comment, the technique only works for values that are not falsy. If values such as 0 or false are acceptable values, you will have to explicitly test for undefined instead.

Call callback if present

Another idiom in Javascript, especially in Node, is passing callbacks to other functions. But, sometimes the callbacks past in are optional. In this case we can use short-circuiting and, &&, instead.

function updateStatistics(data, callback) {
  var result = doSomethingWithData(data);
  // Call the callback if it is defined
  if (callback) return callback(result);
}

function (data, callback) {
  var result = doSomethingWithData(data);
  // The last evaluated value of the `&&` is returned
  return callback && callback(result);
}

I, personally, prefer the first form with the explicit if because I think it communicates my intent better but, it is good to know about the technique anyway.

Update 2013-04-13, A better use for the technique is for testing for the presence of objects before getting their properties.

function callService(url, options) {
  ajaxCall(url, options && options.callback);
}

Lookup Tables

If you have code that behaves differently based on the value of a property, it can often result in conditional statements with multiple else ifs or a switch cases.

if (kind === 'baird')
  bairdBehavior();
else if (kind === 'malayan')
  malayanBehavior();
else if (kind === 'mountain')
  mountainBehavior();
else if (kind === 'lowland')
  lowlandBehavior();
else
  throw new Error('Invalid kind ' + kind);

I find this kind of code ugly and I don't think it looks any better with a switch statement. I prefer to use a lookup table if there is more than two options.

var kinds = {
  baird: bairdBehavior,
  malayan: malayanBehavior,
  mountain: mountainBehavior,
  lowland: lowlandBehavior
};

var func = kinds[kind];
if (!func)
  throw new Error('Invalid kind ' + kind);
func();

I find this code a lot clearer since makes it clear that the else clause handles an exceptional case and that the normal cases works similarly.

Missing Objects

If similar conditionals appear in multiple places in my code, it is a sign that I am missing an object somewhere. Since Javascript is duck typed I can use the same technique as above to create objects instead of just functions.

var kinds = {
  baird: { act: bairdBehavior, info: bairdInfo },
  malayan: { act: malayanBehavior, info: malayanInfo },
  mountain: { act: mountainBehavior, info: mountainInfo },
  lowland: { act: lowlandBehavior, info: lowlandInfo },
};

var tapir = kinds[kind];
if (!tapir)
  throw new Error('Invalid kind of tapir ' + kind);

I prefer to have this kind of code on the borders of my application. That way the code inside my core domain doesn't have to deal with complicated conditional logic. Polymorphism for the win!

Null Objects

If I notice that in many places I have to check fornulls, it is usually a sign that I haven't handled the special null case properly. In the example above I have handled it properly since I throw an Error if the kind of tapir does not exist. But sometimes it is not an error when the value is missing.

// If a non existant kind is used, tapir will become null
var tapir = kinds[kind];

// In other places of the code
if (tapir)
  tapir.act();

// Somewhere else
if (tapir)
  return tapir.info();

This type of code is rather unpleasant and it is time to break out the Null Object.

var tapir = kinds[kind];
// If a non existant kind is used I use a Null Object
if (!tapir)
  tapir = { act: doNothing, info: unknownTapirInfo }

// In other places of the code the conditionals are gone.
tapir.act();

// Somewhere else, no special case here.
return tapir.info();

Null Objects are not appropriate everywhere but, I often find it very enlightening to have them in mind when I write code.

Summary

There are a lot of elegant ways to deal with conditional code in Javascript. I didn't even mention inheritance since it works similarly to the object approach I showed above. But if I need multiple instances of something I would of course use polymorphism through inheritance instead.

8 comments:

  1. just to keep things in line for you

    var func = kinds[kind];
    if (!func)
    throw new Error('Invalid kind ' + kind);
    func();

    ->

    var func = kinds[kind];
    func && func() || throw new Error('Invalid kind ' + kind);

    ReplyDelete
  2. @Rhys, I usually only use the operator syntax when I actually care about the return values.

    In the case where I am throwing the Error I don't care about the return value of func().

    In any case it won't parse :(

    func && func() || throw new Error('Invalid kind ' + kind);
    SyntaxError: Unexpected token throw

    ReplyDelete
  3. ah true - throw doesn't work, not even in a ternary. You could always have a global function that errors for you:

    err = function(error) {
    console.log('error in function' + err.callee);
    throw new Error(error);
    ;};

    and use that instead to make it work then do:

    func ? func() : err("invalid kind " + kind);

    though a better way would probably be to have an assert at the start of your function, then you can just have:

    assert(kind[kind]);
    kind[kind]();

    or if you use closure then you can have an enum with the values, something like:

    /**
    * @enum {string}
    */
    kinds.types = {
    BAIRD: 'baird',
    MALAYAN: 'malayan'
    }

    then you can just have a check in the jsdoc that should get picked up by the compiler:

    /**
    * @param {kinds.types}
    */
    var runType = function(type) {
    kind[type]();
    }

    and call it with something like:

    runType(kinds.types.BAIRD);

    and any errors will get picked up at compile time.

    ReplyDelete
  4. Note that in the callback example, the two functions don't have the same return value if callback is falsy (first will return undefined, second will return callback).

    ReplyDelete
  5. @Rhys, Ah, cool, I didn't know that Closure could pick up on that.

    ReplyDelete
  6. @Golmote, Yes, thats true, you have to beware if it matters to you.

    ReplyDelete
  7. @Rhys: Note that

    COND && FUNC() || ERROR();

    demands that FUNC() return a true value. So even if COND is true, if FUNC() returns a false value, ERROR() will be called.

    COND ? FUNC() : ERROR();

    gets around that problem.

    ---

    My comment on the article itself is that it assumes 0 or false is never a valid value to be passed to a function as an argument; it always replaces 0 or false with the default value. I tend to check for null or undefined (based on my needs), or I use the 'arguments' built-in.

    ReplyDelete
  8. @jefferey, you are write about the falsy values, it is worth pointing out. Thanks!

    ReplyDelete