Wednesday 17 January 2018

Function overloading in Javascript - Best practices

Function Overloading via Dynamic Polymorphism in 100
lines of JS


This is from a larger body of code which
includes the isFn, isArr, etc. type
checking functions. The VanillaJS version below has been reworked to remove all external
dependencies, however you will have to define you're own type checking functions for use
in the .add()
calls.


Note: This is a
self-executing function (so we can have a closure/closed scope), hence the assignment to
window.overload rather than function overload()
{...}
.


window.overload =
function () {
"use strict"
var a_fnOverloads = [],

_Object_prototype_toString = Object.prototype.toString
;
function
isFn(f) {
return (_Object_prototype_toString.call(f) === '[object
Function]');
} //# isFn
function isObj(o) {
return !!(o
&& o === Object(o));
} //# isObj
function isArr(a)
{
return (_Object_prototype_toString.call(a) === '[object
Array]');
} //# isArr
function mkArr(a) {
return
Array.prototype.slice.call(a);
} //# mkArr
function fnCall(fn,
vContext, vArguments) {
//#
//# See:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#Browser_compatibility

vArguments = (isArr(vArguments) ? vArguments : mkArr(vArguments));
if
(isFn(fn)) {
return fn.apply(vContext || this, vArguments);

}
} //# fnCall
//#
function registerAlias(fnOverload,
fn, sAlias) {
//#
if (sAlias && !fnOverload[sAlias])
{
fnOverload[sAlias] = fn;
}
} //#
registerAlias
//#
function overload(vOptions) {
var
oData = (isFn(vOptions) ?
{ default: vOptions } :
(isObj(vOptions)
?
vOptions :
{
default: function (/*arguments*/)
{
throw "Overload not found for arguments: [" + mkArr(arguments) +
"]";
}
}
)
),
fnOverload =
function (/*arguments*/) {
var oEntry, i, j,
a =
arguments,
oArgumentTests = oData[a.length] || []
;
//#
Traverse the oArgumentTests for the number of passed a(rguments), defaulting the oEntry
at the beginning of each loop
for (i = 0; i < oArgumentTests.length; i++)
{
oEntry = oArgumentTests[i];
//# Traverse the passed a(rguments),
if a .test for the current oArgumentTests fails, reset oEntry and fall from the
a(rgument)s loop
for (j = 0; j < a.length; j++) {
if
(!oArgumentTests[i].tests[j](a[j])) {
oEntry = undefined;

break;
}
}
//# If all of the a(rgument)s passed the
.tests we found our oEntry, so break from the oArgumentTests loop
if (oEntry)
{
break;
}
}
//# If we found our oEntry
above, .fn.call its .fn
if (oEntry) {
oEntry.calls++;

return fnCall(oEntry.fn, this, a);
}
//# Else we were unable to
find a matching oArgumentTests oEntry, so .fn.call our .default
else
{
return fnCall(oData.default, this, a);
}
} //#
fnOverload
;
//#
fnOverload.add = function (fn,
a_vArgumentTests, sAlias) {
var i,
bValid = isFn(fn),

iLen = (isArr(a_vArgumentTests) ? a_vArgumentTests.length : 0)
;

//#
if (bValid) {
//# Traverse the a_vArgumentTests, processinge
each to ensure they are functions (or references to )
for (i = 0; i <
iLen; i++) {
if (!isFn(a_vArgumentTests[i])) {
bValid =
_false;
}
}
}
//# If the a_vArgumentTests
are bValid, set the info into oData under the a_vArgumentTests's iLen
if
(bValid) {
oData[iLen] = oData[iLen] || [];

oData[iLen].push({
fn: fn,
tests: a_vArgumentTests,

calls: 0
});
//#
registerAlias(fnOverload, fn,
sAlias);
return fnOverload;
}
//# Else one of the
passed arguments was not bValid, so throw the error
else {
throw
"poly.overload: All tests must be functions or strings referencing `is.*`.";

}
}; //# overload*.add
//#
fnOverload.list = function
(iArgumentCount) {
return (arguments.length > 0 ? oData[iArgumentCount] ||
[] : oData);
}; //# overload*.list
//#

a_fnOverloads.push(fnOverload);
registerAlias(fnOverload, oData.default,
"default");
return fnOverload;
} //# overload

//#
overload.is = function (fnTarget) {
return
(a_fnOverloads.indexOf(fnTarget) > -1);
} //# overload.is

return
overload;
}();

Usage:


The
caller defines their overloaded functions by assigning a variable to the return of
overload(). Thanks to chaining, the additional overloads can be
defined in series:


var myOverloadedFn =
overload(function(){ console.log("default", arguments) })
.add(function(){
console.log("noArgs", arguments) }, [], "noArgs")
.add(function(){
console.log("str", arguments) }, [function(s){ return typeof s === 'string' }],
"str")
;

The single
optional argument to overload() defines the "default" function
to call if the signature cannot be identified. The arguments to
.add()
are:



  1. fn:
    function defining the
    overload;

  2. a_vArgumentTests:
    Array of functions defining the tests
    to run on the arguments. Each function
    accepts a single argument and returns truethy based on if the
    argument is valid;

  3. sAlias
    (Optional): string defining the alias to directly access the
    overload function (fn), e.g.
    myOverloadedFn.noArgs() will call that function directly,
    avoiding the dynamic polymorphism tests of the
    arguments.


This implementation
actually allows for more than just traditional function overloads as the second
a_vArgumentTests argument to .add() in
practice defines custom types. So, you could gate arguments not only based on type, but
on ranges, values or collections of values!


If you look
through the 145 lines of code for overload() you'll see that
each signature is categorized by the number of arguments passed
to it. This is done so that we're limiting the number of tests we are running. I also
keep track of a call count. With some additional code, the arrays of overloaded
functions could be re-sorted so that more commonly called functions are tested first,
again adding some measure of performance enhancement.


Now,
there are some caveats... As Javascript is loosely typed, you will have to be careful
with your vArgumentTests as an integer
could be validated as a float,
etc.


JSCompress.com version (1114 bytes, 744 bytes
g-zipped):


window.overload=function(){'use
strict';function b(n){return'[object Function]'===m.call(n)}function
c(n){return!!(n&&n===Object(n))}function d(n){return'[object
Array]'===m.call(n)}function e(n){return Array.prototype.slice.call(n)}function
g(n,p,q){if(q=d(q)?q:e(q),b(n))return n.apply(p||this,q)}function
h(n,p,q){q&&!n[q]&&(n[q]=p)}function k(n){var
p=b(n)?{default:n}:c(n)?n:{default:function(){throw'Overload not found for arguments:
['+e(arguments)+']'}},q=function(){var
r,s,t,u=arguments,v=p[u.length]||[];for(s=0;s 0;break}if(r)break}return r?(r.calls++,g(r.fn,this,u)):g(p.default,this,u)};return
q.add=function(r,s,t){var
u,v=b(r),w=d(s)?s.length:0;if(v)for(u=0;u p[w]=p[w]||[],p[w].push({fn:r,tests:s,calls:0}),h(q,r,t),q;throw'poly.overload: All
tests must be functions or strings referencing `is.*`.'},q.list=function(r){return
0 l=[],m=Object.prototype.toString;return
k.is=function(n){return-1

No comments:

Post a Comment

php - file_get_contents shows unexpected output while reading a file

I want to output an inline jpg image as a base64 encoded string, however when I do this : $contents = file_get_contents($filename); print &q...