Handling the unexpected - Type safe functions in Javascript
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.
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.
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:
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:
// 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
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.
Attachment | Size |
---|---|
jquery.makeTypeSafe.js.txt | 4.55 KB |
- Login to post comments
Comments
Anonymous - Wed, 08/05/2009 - 14:31
On a sidenote and you do not want to use the typesafe function wrapper, instead of doing this:
You can do like this:
A little cleaner IMHO.
Matthias Reuter - Thu, 08/06/2009 - 11:17
I wanted to keep my code easy to understand, so I explicitely split this in two parts.
In a draft version I had this:
return n + m;
}
which is correct but somewhat obscure.
Anonymous - Thu, 08/06/2009 - 21:28
That is sweet. I'm a simple douche but would have preferred that you had this code in your demo,
Stephan (not verified) - Tue, 12/13/2011 - 21:40
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 (not verified) - Mon, 05/03/2010 - 09:12
You have a typo in your makeTypeSafe function: this line
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:
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 (not verified) - Sat, 03/19/2011 - 02:43
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 (not verified) - Tue, 12/13/2011 - 21:45
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. :)