PDA

View Full Version : Function Overloading


Trinithis
07-20-2009, 09:54 PM
I wrote some code to deal with general function (and method) overloading in Javascript. You can overload based on arity, types, optional arguments, and variable length arguments.

Overload Syntax:

var foo = overload (
[Type1, Type2, Type3]
, function (x, y, z) { ... }
, [Type4]
, function (x) { ... }
/* etc */
)


Type Syntax:

Type // match the type
[Type, defaultValue] // try matching Type. Else use default value.
[Type] // variable amount of Type
nullable (Type) // accepts Type or null
[[function (x) { return x.length > 5; }]] // Custom predicate.


All matching is done greedily.

Object will not match primitive numbers, primitive booleans, primitive strings, null, or undefined.

String, Number, Boolean, and Function will match against primitives and objects of the corresponding types.

Varargs are condensed into array format before tossed to corresponding functions.

Custom predicates are executed in try-catch blocks, allowing you to not fuss over whether or not your predicate pulling non-existent attributes and other things.

ANYTYPE will match anything. It will even match null.

overload.js:

var nullable, ANYTYPE;

var overload = (function () {

ANYTYPE = {};

var extend = function (child, parent) {
child.prototype = new parent ();
child.prototype.constructor = child;
};

var combine = function (x, xs) {
if (xs) {
xs.push (x);
}
return xs;
};

var Type = function (constr, nextType) {
this.constr = constr;
this.nextType = nextType;
};

Type.prototype = {
constructor: Type
, match: function (arg) {
if (arg === undefined || arg === null) {
return false;
}
return arg instanceof this.constr;
}
, /* Builds arglist in REVERSE if compatable!!! Else returns false. */
unify: function (args, i) {
return this.match (args [i])
? combine (args [i], this.nextType.unify (args, i + 1))
: null
;
}
};


var EndType = function () {
Type.call (this, null, this);
};

extend (EndType, Type);
EndType.prototype.match = function (arg) {
return false;
};
EndType.prototype.unify = function (args, i) {
return args.length === i
? []
: null
;
};

var ENDTYPE = new EndType ();


var Nullable = function (constr, nextType) {
Type.call (this, constr, nextType);
};

extend (Nullable, Type);
Nullable.prototype.match = function (arg) {
return arg === null || Type.prototype.match.call (this, arg);
};

nullable = function (constr) {
return new Nullable (constr);
};


var AnyType = function (nextType) {
Type.call (this, null, nextType);
}

extend (AnyType, Type);
AnyType.prototype.match = function (arg) {
return true;
};


var Primitive = function (constr, nextType) {
Type.call (this, constr, nextType);
switch (constr) {
case String:
this.typeOf = "string";
break;
case Number:
this.typeOf = "number";
break;
case Boolean:
this.typeOf = "boolean";
break;
case Function:
this.typeOf = "function";
break;
}
};

extend (Primitive, Type);
Primitive.prototype.match = function (arg) {
return typeof arg === this.typeOf || Type.prototype.match.call (this, arg);
};


var Predicate = function (pred, nextType) {
Type.call (this, null, nextType);
this.pred = pred;
}

extend (Predicate, Type);
Predicate.prototype.match = function (arg) {
try {
return this.pred (arg);
}
catch (e) {
return false;
}
};


var TypeWrapper = function (type, nextType) {
Type.call (this, null, nextType);
this.type = type;
};

extend (TypeWrapper, Type);
TypeWrapper.prototype.match = function (arg) {
return this.type.match (arg);
};


var Optional = function (type, nextType, defaultValue) {
TypeWrapper.call (this, type, nextType);
this.defaultValue = defaultValue;
};

extend (Optional, TypeWrapper);
Optional.prototype.unify = function (args, i) {
if (this.match (args [i])) {
return combine (args [i], this.nextType.unify (args, i + 1));
}
return combine (this.defaultValue, this.nextType.unify (args, i));
};


var Variadic = function (type, nextType) {
TypeWrapper.call (this, type, nextType);
};

extend (Variadic, TypeWrapper);
Variadic.prototype.unify = function (args, i) {
var result;
var maxJ = args.length;
do {
var j = i;
var argGroup = [];
while (j < maxJ && this.type.match (args [j])) {
j = argGroup.push (args [j]);
}
maxJ = j - 1;
result = combine (argGroup, this.nextType.unify (args, j));
if (result) {
return result;
}
} while (maxJ >= i);
return result;
};


var parseType = function (sig, nextType) {
if (sig instanceof Array) {
if (sig [0] instanceof Array) {
return new Predicate (sig [0] [0], nextType);
}
else if (sig.length === 1) {
return new Variadic (parseType (sig [0]), nextType);
}
else if (sig.length === 2) {
return new Optional (parseType (sig [0]), nextType, sig [1]);
}
}
else if (sig === ANYTYPE) {
return new AnyType (nextType);
}
else if (sig.constructor === Nullable) {
return new Nullable (sig.constr, nextType);
}
else if (sig === String || sig === Number || sig === Boolean || sig === Function) {
return new Primitive (sig, nextType);
}
else {
return new Type (sig, nextType);
}
};

var mkTypeChain = function (sigs) {
var chain = ENDTYPE;
for (var i = sigs.length - 1; i >= 0; --i) {
chain = parseType (sigs [i], chain);
}
return chain;
};

return function () {
if (arguments.length % 2 !== 0) {
throw new Error ("overload: Invalid number of arguments.");
}
var typeChains = [];
var funcs = [];
for (var i = 0; i < arguments.length; i += 2) {
typeChains.push (mkTypeChain (arguments [i]));
funcs.push (arguments [i + 1]);
}
return function () {
for (var i = 0; i < typeChains.length; ++i) {
var result = typeChains [i].unify (arguments, 0);
if (result) {
result.reverse ();
return funcs [i].apply (this, result);
}
}
};
};
}) ();


Example:

var foo = overload (
[[[function (x) { return x.length == 1; }]]]
, function (chr) {
return [1, chr];
}
, [[Number, 777], String]
, function (num, str) {
return [2, num, str];
}
, [String, String, String]
, function (str1, str2, str3) {
return [3, str1, str2, str3];
}
, [String, String, Number]
, function (str1, str2, num) {
return [4, str1, str2, num];
}
, [[Number]]
, function (nums) {
return [5, nums.toSource ()];
}
, [[Number], Number, String]
, function (nums, num, str) {
return [6, nums.toSource (), num, str];
}
, [Object, Object]
, function (obj1, obj2) {
return [7, obj1, obj2];
}
, [nullable (Object)]
, function (obj) {
return [8, String (obj)];
}
, [ANYTYPE, Object]
, function (obj1, obj2) {
return [9, obj1, obj2];
}
);

document.write (foo ("x"), "<br />"); // 1,x
document.write (foo ("hello"), "<br />"); // 2,777,hello
document.write (foo (888, "hello"), "<br />"); // 2,888,hello
document.write (foo ("a", "b", "c"), "<br />"); // 3,a,b,c
document.write (foo ("a", "b", 666), "<br />"); // 4,a,b,666
document.write (foo (), "<br />"); // 5,[]
document.write (foo (222), "<br />"); // 5,[222]
document.write (foo (22, 33, 44, 55), "<br />");// 5,[22,33,44,55]
document.write (foo (11, 22, 33, 44, "a"), "<br />");//6,[11,22,33],44,a
document.write (foo ({}, /regex/), "<br />"); // 7,[object Object],/regex/
document.write (foo (/regex/), "<br />"); // 8,/regex/
document.write (foo (null), "<br />"); // 8,null
document.write (foo (true, /regex/), "<br />"); // 9,[object Object],/regex/

rnd me
07-22-2009, 12:21 AM
very interesting.

i think some real-world examples of this code in action would prove instructional to most of our readers.

Trinithis
07-22-2009, 02:49 AM
I fixed a bug in the code, and here are some better examples.

Example 1:

var clip = overload (
[[Number, 0], Array, [Number, 0]]
, function (fromStart, xs, fromEnd) {
var max = xs.length - fromEnd;
var ys = []
for (var i = fromStart; i < max; ++i) {
ys.push (xs [i]);
}
return ys;
}
);

var xs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
clip (xs);
clip (4, xs);
clip (xs, 7);
clip (2, xs, 3);

Results 1:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9
4,5,6,7,8,9
0,1,2
2,3,4,5,6


Example 2:

var serialize = overload (
[Number]
, function (n) {
return n.toString (16).toUpperCase ();
}
, [String]
, function (str) {
var es = "nrtf\\\"";
for (var i = 0; i < es.length; ++i) {
var e = es.charAt (i);
str = str.replace (new RegExp ("\\" + e, "g"), "\\" + e);
}
return '"' + str + '"';
}
, [Boolean]
, function (bool) {
return bool ? 1 : 0;
}
, [Function]
, function (func) {
return "{FUNCTION}";
}
, [Array]
, function (xs) {
var ys = [];
for (var i = 0; i < xs.length; ++i) {
ys.push (serialize (xs [i]));
}
return '[' + ys.join (',') + ']';
}
, [Object]
, function (obj) {
return obj.toString ();
}
, [ANYTYPE]
, function (x) {
return String (x);
}
, [Number, ANYTYPE]
, function (n, x) {
return serialize (x).substring (n);
}
, [[ANYTYPE]]
, function (xs) {
return serialize (xs);
}
);

serialize (7894456548774);
serialize (4, 7894456548774);
serialize (666, "NAME:\t\"Gerald\"", true, serialize, [1, 2, 3], {}, null, undefined);

Results 2:

72E12473DA6
2473DA6
[29A,"NAME:\\t\"Gerald\"",1,{FUNCTION},[1,2,3],[object Object],null,undefined]

Trinithis
07-22-2009, 03:15 AM
Example 3:

While I wouldn't bother using overload for the following, here it is anyway just for comparison. I guess it's a tradeoff between speed (which I doubt most scripts care about) and elegance.

By hand:

Function.bundle = function (context, f, args) {
if (typeof f == "string") {
f = context [f];
}
if (arguments.length < 3) {
args = [];
}
else if (!(args instanceof Array)) {
args = Array.prototype.slice.call (arguments, 2);
}
return function () {
return f.apply (context, args);
};
};


Using overload:

Function.bundle = overload (
[ANYTYPE, Function, Array]
, function (context, f, args) {
return function () {
return f.apply (context, args);
};
}
, [ANYTYPE, String, Array]
, function (context, f, args) {
return Function.bundle (context, context [f], args);
}
, [ANYTYPE, ANYTYPE, [ANYTYPE]]
, function () {
return Function.bundle.apply (this, arguments);
}
);


BTW Function.bundle is an awesome tool :D.

Trinithis
07-23-2009, 12:15 PM
I've done a major rehaul of the code. I redid everything OO style. New code is now shown in the original post, replacing the old code.

For those who want the old code, here it is:

function Nullable (type) {
this.type = type;
}

function nullable (type) {
return new Nullable (type);
}

var ANYTYPE = {};

var overload = (function () {

var combine = function (x, xs) {
if (xs === false) {
return false;
}
xs.push (x);
return xs;
};

var toTypeof = function (type) {
switch (type) {
case Boolean: return "boolean";
case Function: return "function";
case Number: return "number";
case String: return "string";
default: return null;
}
};

var isPredicate = function (type) {
return type instanceof Array && type [0] instanceof Array;
};

var getPredicate = function (type) {
return type [0][0];
};

var isNullable = function (type) {
return type instanceof Nullable;
};

var isOptional = function (type) {
return type instanceof Array && type.length == 2 && !isPredicate (type);
};

var isVariadic = function (type) {
return type instanceof Array && type.length == 1 && !isPredicate (type);
};

var getType = function (typeWrapper) {
return typeWrapper [0];
};

var defaultValue = function (optionalType) {
return optionalType [1];
};

var match = function (arg, type) {
if (isPredicate (type)) {
try {
return getPredicate (type) (arg);
}
catch (e) {
return false;
}
}
if (type == ANYTYPE) {
return true;
}
if (isNullable (type)) {
return arg === null || match (arg, type.type);
}
if (arg === null || arg === undefined || type === null || type === undefined) {
return false;
}
if (isOptional (type) || isVariadic (type)) {
type = getType (type);
}
return arg instanceof type || typeof arg == toTypeof (type);
};

/* Builds arglist in REVERSE if compatable!!! Else returns false */
var unify = function (args, types, i, j) {
if (i == args.length && j == types.length) {
return [];
}
var arg = args [i];
var type = types [j];
if (isOptional (type)) {
return unifyOptional (args, types, i, j);
}
else if (isVariadic (type)) {
return unifyVariadic (args, types, i, j);
}
else {
return match (arg, type)
? combine (arg, unify (args, types, i + 1, j + 1))
: false
;
}
};

var unifyOptional = function (args, types, i, j) {
var arg = args [i];
var type = types [j];
if (match (arg, type)) {
var result = combine (arg, unify (args, types, i + 1, j + 1));
if (result) {
return result;
}
}
return combine (defaultValue (type), unify (args, types, i, j + 1));
};

var unifyVariadic = function (args, types, i, j) {
var type = types [j];
var maxK = Number.POSITIVE_INFINITY;
do {
var argGroup = [];
var k = 0;
while (k < maxK && (i + k) < args.length && match (args [i + k], type)) {
k = argGroup.push (args [i + k]);
}
maxK = k - 1;
var result = combine (argGroup, unify (args, types, i + k, j + 1));
if (result) {
return result;
}
} while (maxK > 0);
return combine ([], unify (args, types, i, j + 1));
};

return function () {
var defns = arguments;
return function () {
for (var i = 0; i < defns.length; i += 2) {
var types = defns [i];
var func = defns [i + 1];
var result = unify (arguments, types, 0, 0);
if (result) {
result.reverse ();
return func.apply (this, result);
}
}
};
};
}) ();

Trinithis
07-23-2009, 09:55 PM
I just tested the speed differences between my old and new code (Linux Firefox 3). The new code is significantly faster. It always runs faster than the old code and often runs about twice as fast.

rnd me
07-24-2009, 03:32 AM
I just want to say that this code is really cool.

I don't come across much javascript like this, and it's nice to see novel designs instead of spaghetti. The declarative style of the overload versions is very refreshing and intuitive. I think it will come in quite handy for tokenizing code and transforming data.

I think it also allows more portable code to be authored.

You would only need to update overload() to play nice on new implementations while your program code remains unchanged. You can then optimize overload() for ecma5, or asp js, or whatever, and your whole app speeds up.

once again, thanks for putting this out there, i appreciate it.

Element
09-07-2009, 09:31 AM
Wow, this is awesome, thanks so much!