More Useful Function Patterns: Function Overloading
One same API,
to provide similar function,
that's a smart thing,
memorable, familiar, consistent
- Siwelluap
Chieftain of the twisted fangs
Have you Heard About The Marvels Of Overloading?
In the last couple of chapters we learned some useful patterns with functions in JavaScript that helped us achieve defaults and handling arbitrary arguments. We also saw a common thread: The fact that ES6 comes with a lot of new features that make up for past limitations of the language. Features like native defaults and rest parameters that let you solve these old problems in a more concise style.
This chapter will close this section - useful function patterns - with some tips on how you can achieve function overloading in JavaScript.
Function overloading helps you reuse a piece of functionality and provide a unified API in those situations when you have slightly different arguments yet you want to achieve the same thing. Unfortunately, there’s a problem with function overloading in JavaScript.
The Problem with Function Overloading in JavaScript
There’s a slight issue when you attempt to do function overloading in JavaScript like you would in C#. You can’t do it.
Indeed, one does not simply overload functions in JavaScript willy nilly. Imagine a spell to raise a skeleton army:
1 function raiseSkeleton(){
2 console.log('You raise a skeleton!!!');
3 }
And now imagine that you want to overload it to accept an argument mana that will affect how many skeletons can be raised from the dead at once:
1 function raiseSkeleton(mana){
2 console.log('You raise ' + mana + ' skeletons!!!');
3 }
If you now try to execute the raiseSkeleton function with no arguments you would probably expect the first version of the function to be called (just like it would happen in C#). However, what you’ll discover, to your dismay, is that raiseSkeleton has been completely overwritten:
1 raiseSkeleton();
2 // => You raise undefined skeletons!!!
In JavaScript, you cannot override a function by defining a new one with the same name and a different signature. If you try to do so, you’ll just succeed in overwriting your original function with a new implementation.
How Do We Do Function Overloading Then?
Well, as with many things in JavaScript, you’ll need to take advantage of the flexibility and freedom the language gives you to emulate function overloading yourself. In the upcoming sections you’ll learn four different ways in which you can achieve it, each with their own strengths and caveats:
- Inspecting arguments
- Using an options object
- Relying on ES6 defaults
- Taking advantage of polymorphic functions
Function Overloading by Inspecting Arguments
One common pattern for achieving function overloading is to use the arguments object to inspect the arguments that are passed into a function:
1 function raiseSkeletonWithArgumentInspecting(){
2 if (typeof arguments[0] === "number"){
3 raiseSkeletonsInNumber(arguments[0]);
4 } else if (typeof arguments[0] === "string") {
5 raiseSkeletonCreature(arguments[0]);
6 } else {
7 console.log('raise a skeleton');
8 }
9
10 function raiseSkeletonsInNumber(n){
11 console.log('raise ' + n + ' skeletons');
12 }
13 function raiseSkeletonCreature(creature){
14 console.log('raise a skeleton ' + creature);
15 };
16 }
Following this pattern you inspect each argument being passed to the overloaded function(or even the number of arguments) and determine which internal implementation to execute:
1 raiseSkeletonWithArgumentInspecting();
2 // => raise a skeleton
3 raiseSkeletonWithArgumentInspecting(4);
4 // => raise 4 skeletons
5 raiseSkeletonWithArgumentInspecting('king');
6 // => raise skeleton king
This approach can become unwieldy very quickly. As the overloaded functions and their parameters increase in number, the function becomes harder and harder to read, maintain and extend.
At this point you may be thinking: ”…checking the type of the arguments being passed? seriously?!” and I agree with you, that’s why I like to use this next approach instead.
Using an Options Object
A better way to achieve function overloading is to use an options object. This object acts as a container for the different parameters a function can consume:
1 function raiseSkeletonWithOptions(spellOptions){
2 spellOptions = spellOptions || {};
3 var armySize = spellOptions.armySize || 1,
4 creatureType = spellOptions.creatureType || '';
5
6 if (creatureType){
7 console.log('raise a skeleton ' + creatureType);
8 } else {
9 console.log('raise ' + armySize + ' skeletons ' + creatureType);
10 }
11 }
This allows you to call a function with different arguments:
1 raiseSkeletonWithOptions();
2 // => raise a skeleton
3 raiseSkeletonWithOptions({armySize: 4});
4 // => raise 4 skeletons
5 raiseSkeletonWithOptions({creatureType:'king'});
6 // => raise skeleton king
It is not strictly function overloading but it provides the same benefits: It gives you different possibilities in the form of a unified API, and additionally, named arguments and easy extensibility. That is, you can add new options without breaking any existing clients of the function.
Here is an example of both argument inspecting and the options object patterns in the wild, the jQuery ajax function:
1 ajax: function( url, options ) {
2 // If url is an object, simulate pre-1.5 signature
3 if ( typeof url === "object" ) {
4 options = url;
5 url = undefined;
6 }
7
8 // Force options to be an object
9 options = options || {};
10
11 var transport,
12 // URL without anti-cache param
13 cacheURL,
14 // Response headers
15 responseHeadersString,
16 responseHeaders,
17 // timeout handle
18 timeoutTimer,
19 // etc...
20 }
Relying on ES6 Defaults
Although ES6 doesn’t come with classic function overloading, it brings us default arguments which give you better support for function overloading than what we’ve had so far.
If you reflect about it, default arguments are a specialized version of function overloading. A subset of it, if you will, for those cases in which you can use an increasing number of predefined arguments:
1 function castIceCone(mana=5, {direction='in front of you'}={}){
2 console.log(`You spend ${mana} mana and casts a ` +
3 `terrible ice cone ${direction}`);
4 }
5 castIceCone();
6 // => You spend 5 mana and casts a terrible ice cone in front of you
7 castIceCone(10, {direction: 'towards Mordor'});
8 // => You spend 10 mana and casts a terrible ice cone towards Mordor
Taking Advantage of Polymorphic Functions
Yet another interesting pattern for achieving function overloading is to rely on JavaScript great support for functional programming. In the world of functional programming there is the concept of polymorphic functions, that is, functions which exhibit different behaviors based on their arguments.
Let’s illustrate them with an example. Our starting point will be this function that we saw in the inspecting arguments section:
1 function raiseSkeletonWithArgumentInspecting(){
2 if (typeof arguments[0] === "number"){
3 raiseSkeletonsInNumber(arguments[0]);
4 } else if (typeof arguments[0] === "string") {
5 raiseSkeletonCreature(arguments[0]);
6 } else {
7 console.log('raise a skeleton');
8 }
9
10 function raiseSkeletonsInNumber(n){
11 console.log('raise ' + n + ' skeletons');
12 }
13 function raiseSkeletonCreature(creature){
14 console.log('raise a skeleton ' + creature);
15 };
16 }
We will take it and decompose it into smaller functions:
1 function raiseSkeletons(number){
2 if (Number.isInteger(number)){ return `raise ${number} skeletons`;}
3 }
4
5 function raiseSkeletonCreature(creature){
6 if (creature) {return `raise a skeleton ${creature}`;}
7 }
8
9 function raiseSingleSkeleton(){
10 return 'raise a skeleton';
11 }
And now we create an abstraction (functional programming likes abstraction) for a function that executes several other functions in sequence until one returns a valid result. Where a valid result will be any value different from undefined:
1 // This is a higher-order function that returns a new function.
2 // Something like a function factory.
3 // We could reuse it to our heart's content.
4 function dispatch(...fns){
5
6 return function(...args){
7 for(let f of fns){
8 let result = f.apply(null, args);
9 if (exists(result)) return result;
10 }
11 };
12 }
13
14 function exists(value){
15 return value !== undefined
16 }
dispatch lets us create a new function that is a combination of all the previous ones: raiseSkeletons, raiseSkeletonCreature and raiseSingleSkeleton:
1 let raiseSkeletonFunctionally = dispatch(
2 raiseSkeletons,
3 raiseSkeletonCreature,
4 raiseSingleSkeleton);
This new function will behave in different ways based on the arguments it takes. It will delegate any call to each specific raise skeleton function until a suitable result is obtained.
1 console.log(raiseSkeletonFunctionally());
2 // => raise a skeleton
3 console.log(raiseSkeletonFunctionally(4));
4 // => raise 4 skeletons
5 console.log(raiseSkeletonFunctionally('king'));
6 // => raise skeleton king
Note how the last raiseSingleSkeleton is a catch-all function. It will always return a valid result regardless of the arguments being sent to the function. This will ensure that however you call raiseSkeletonFunctionally you’ll always have a default implementation or valid result.
A super duper mega cool thing that you may or may not have noticed is the awesome degree of composability of this approach. If we want to extend this function later on, we can do it without modifying the original function. Take a look at this:
1 function raiseOnSteroids({number=0, type='skeleton'}={}){
2 if(number) {
3 return `raise ${number} ${type}s`;
4 }
5 }
6
7 let raiseAdvanced = dispatch(raiseOnSteroids,
8 raiseSkeletonFunctionally);
We now have a raiseAdvanced function that augments raiseSkeletonFunctionally with the new desired functionality represented by raiseOnSteroids:
1 console.log(raiseAdvanced());
2 // => raise a skeleton
3 console.log(raiseAdvanced(4));
4 // => raise 4 skeletons
5 console.log(raiseAdvanced('king'));
6 // => raise skeleton king
7 console.log(raiseAdvanced({number: 10, type: 'ghoul'}))
8 // => raise 10 ghouls
This is the OCP (Open-Closed Principle)10 in all its glory like you’ve never seen it before. Functional programming is pretty awesome right? We will take a deeper dive into functional programming within the sacred tome of FP later in the series and you’ll get the chance to experiment a lot more with both higher-order functions and function composition alike.
Concluding
Although JavaScript doesn’t support function overloading you can achieve the same behavior by using different patterns: inspecting arguments, using an options object, relying on ES6 defaults or taking advantage of polymorphic functions.
You can use the arguments object and inspect the arguments that are being passed to a function at runtime. You should only use this solution with the simplest of implementations as it becomes unwieldly and hard to maintain as parameters and overloads are added to a function.
Or you can use an options object as a wrapper for parameters. This is both more readable and maintanaible than inspecting arguments, and provides two additional benefits: named arguments and a lot of flexibility to extend the function with new parameters.
ES6 brings improved support for function overloading in some situations with native default arguments.
Finally, you can take advantage of functional programming, compose your functions from smaller ones and use a dispatching mechanism to select which function is used based on the arguments.