Handling the unexpected - Type safe functions in Javascript

Matthias Reuter's picture

Javascript is a weird language. Great but weird. Take functions for example. You cannot only pass any type of arguments, you can also pass any number of arguments. This may be quite disturbing, especially for Java developers. Recently I had a discussion with a Java developer who complained about missing code assistance in Javascript, by which he meant no hint about the type of an argument.

This is of course due to the dynamically typed nature of Javascript. While in Java you denote the expected type of a parameter, and it's the caller's duty to pass the correct type (even more, you cannot pass a different type), in Javascript it's the function's duty to handle the given parameters and do something reasonable with unexpected types. The question arises: How do you write type safe functions in Javascript? Let me explain this by an example implementation to calculate the greatest common divisor of two numbers.

[The greatest common divisor of two integers is the largest integer that devides these integers without a remainder. The greatest common divisor (or gcd for short) of 24 and 15 is 3, since 24 = 3 · 8 and 15 = 3 · 5.]

This is a basic implementation to calculate the greatest common divisor. It's based upon the fact that gcd(n,m) = gcd(n-m,m) and gcd(n,n) = n. This is a very easy algorithm, though not the fastest.

function greatestCommonDivisor (n, m) {
  if (n === m) {
    return n;
  }
  if (n < m) {
    return greatestCommonDivisor(n, m - n);
  }
  return greatestCommonDivisor(n - m, n);
}

So now, how do you make this function type safe? It's a function for integers, how do you handle, for example, strings?

Leave it to the user

The easiest answer is, you don't. Just leave the responsibility to the user. If he's to dumb to provide integers let him handle the consequences.

[By the way, what happens when calling this function with strings? Interestingly, you might get the right result, greatestCommonDivisor("24", "15") returns 3, but this is sheer luck. greatestCommonDivisor("9", "24") results in an infinite loop.]

The consequences could be (a) the right result (unlikely but possible), (b) an error thrown (ugly, but the user asked for it), (c) unexpected results like NaN, null or undefined (still ugly) or (d) an infinite loop. This last possibility should make us discard the easy answer. We might accept a (we surly would), b and c; d however is out of question.

Type check arguments

So the second answer is to type check the arguments. We want numbers, so we reject anything else. How do we reject unexpected types? Throw an error, at least for now.

function greatestCommonDivisor (n, m) {
  if (typeof n !== "number" || typeof m !== "number") {
    throw "Arguments must be numbers";
  )

  if (n === m) {
    return n;
  }
  if (n < m) {
    return greatestCommonDivisor(n, m - n);
  }
  return greatestCommonDivisor(n - m, n);
}

Convert arguments

While this is a valid solution, there are better. If n and m come from user input, it's likely they might be strings. Strings can be easily converted to numbers, so instead of rejecting them, we should convert them. This can be done by calling Number as a functíon:

function greatestCommonDivisor (n, m) {
  n = Number(n);
  m = Number(m);

  // We had to change that check, since Number() might return NaN
  if (isNaN(n) || isNaN(m)) {
    throw new TypeError("Arguments must be numbers");
  )

  if (n === m) {
    return n;
  }
  if (n < m) {
    return greatestCommonDivisor(n, m - n);
  }
  return greatestCommonDivisor(n - m, n);
}

Better design

So now you know three concepts of handling unexpected types: Ignore them (might result in infinitive loops, bad), reject them (why give up so easily, bad) and convert them (good). The concept might be good, but the implementation lacks something. I'll show you what.

First (and that is a minor point) the algorithm is bad. If you pass negative numbers or zero, it results in an infinitive loop. This can be solved by taking the absolute value (gcd(n,m) equals gcd(-n,m)).

Second (a minor point as well) the greatest common divisor expects integers but conversion to number results in float values. This can be solved by using parseInt() instead of Number().

The third point is a major. The function greatestCommonDivisor is called recursively. That means in every call we type convert and check the arguments, although after the first call we know the arguments have the correct type. There is an elegant solution. First, we convert and check the arguments. Then we define an inner function to do the calculation, which is then called recursively. Since we provide the arguments for the inner function, we don't have to check them again there.

The fourth is a minor point again. When dealing with numbers I'd rather not throw an error but return NaN instead. If the user provides arguments which result in NaN, we should just tell him so.

Therefore we get the following solution to how to write a type safe function in Javascript:

function greatestCommonDivisor (n, m) {
  // convert n to an integer using parseInt,
  // take the absolute value to prevent infinitive loops
  n = Math.abs(parseInt(n, 10));
 
  // do the same for m
  m = Math.abs(parseInt(m, 10));

  // check if conversion lead to NaN
  if (isNaN(n) || isNaN(m)) {
    return NaN;
  )
 
  // prevent infinitve loop when one argument is zero
  if (n === 0) {
    return m;
  }
  if (m === 0) {
    return n;
  }
 
  // now the inner function
  var gcd = function (n, m) {
    // gcd(n,1) is 1, so prevent recursion to speed things up
    if (n === 1 || m === 1) {
      return 1;
    }
    if (n === m) {
      return n;
    }
    if (n < m) {
      return gcd(n, m - n);
    }
    return gcd(n - m, n);
  };
 
  // invoke the inner function
  return gcd(n, m);
}

There, we're done! Now... wouldn't it be great to make type checking automated? Something like

var typeSafeGcd = makeTypeSafe(greatestCommonDivisor, ["int", "int"]);
typeSafeGcd(21, 15);       // would still return 3
typeSafeGcd(21.5, "abc");  // would fail

Yes, this can be done and I will show you how. There are some drawbacks though. First, how do you automatically convert an unexpected type? It's easy for some types, for example if you expect a number. However, how do you convert a string to an array? Simply wrap the string? Split it somehow? Converting requires some thought and depends on the context. Therefore I think it's not a good idea to do so automatically. That leaves checking types and rejecting unsupported ones.

Second, how do you reject unsupported types? You could either return null, undefined or NaN (depending on the context) or throw an error. Both ways lead to the user having to check the results of function calls. In other words, you push the responsibility to provide correct parameters off to the user. Graceful conversion definitely is the better alternative.

So keep on reading, if you still want to know how to automatically make a function type safe.

AttachmentSize
jquery.makeTypeSafe.js.txt4.55 KB

Comments

Anonymous's picture

On a sidenote and you do not want to use the typesafe function wrapper, instead of doing this:

  1. if (n === 0) {
  2.     return m;
  3.   }
  4.   if (m === 0) {
  5.     return n;
  6.   }

You can do like this:

  1. if (n === 0 || m === 0) {
  2.     return  Math.max(n,m)
  3.   }

A little cleaner IMHO.

Matthias Reuter's picture

I wanted to keep my code easy to understand, so I explicitely split this in two parts.

In a draft version I had this:

if (n * m === 0) {
  return n + m;
}

which is correct but somewhat obscure.

Anonymous's picture

That is sweet. I'm a simple douche but would have preferred that you had this code in your demo,

Stephan's picture

I totally agree with you that explicitly checking n and then m for being 0 is more readable. Often people think, they are clever using fever lines to express the same, but often it just obfuscates the code. It's not every day I find people of the same opinion as I, so I thought it was worth giving a reply on this one :)

Great points in your post!

Vincent's picture

You have a typo in your makeTypeSafe function: this line

  1. throw "Unexpected number of arguments. Expected " + p + ", got " + arguments.length;

contains 'p', which should be parameterList.length.

Btw, I have done similar stuff, yet not completely generic so far. However, I preferred using object references instead of strings for types, like:

  1. function ObjectA(...) { ... }
  2.  
  3. makeTypeSafe(f, [ObjectA]);

This makes the type checking functions superfluous (note, however, that basic types like int are somewhat sloppy in javascript, so you will require explicit casting to an object variant for these).

The Wishmaster's picture

Don't try to elevate javascript status to a program language. It's just a script language, a simple interpreted language highly browser dependent. All this stuff is cool, but quite useless to real world since we have many more options for problems like that. GCD is just academic example that, we all know, don't apply into real world.
That's my opinion. Great article at all.

Stephan's picture

JavaScript is Touring complete which makes is as much a programming language as any other language. And with node.js, JavaScript is no longer restricted for use in browsers. :)