mire
Version:
Generic functions in JavaScript
194 lines (171 loc) • 6.39 kB
JavaScript
/* jshint node: true, esversion: 6 */
;
const {inspect} = require('util'),
Dispatcher = require('./dispatcher'),
{isArray} = Array;
/**
* Creates a generic function.
* @param {Object} opt - the option object of the generic function.
* @param {string} [opt.name = ''] - the name of the generic function.
* @param {number} [opt.length = 0] - the length of the generic function.
* @param {Function} [opt.fallback = throwingHandler] - the fallback handler to be used when no other handler matches the arguments.
* @return {Function} a generic function.
**/
function create(opt = {}) {
const {name, length, fallback} = opt;
if (fallback && !isFunction(fallback)) {
throw fallbackNotAFunctionError(fallback);
}
return createValidated({
name: name || '',
length: length || 0,
fallback: fallback
});
}
/**
* Creates a generic function from a common function.
* @param {Function} fn - the function that will be promoted to a generic function.
* @return {Function} a generic function.
**/
function of(fn) {
if (!isFunction(fn)) {
throw promotingNotAFunctionError(fn);
}
return createValidated({
name: fn.name,
length: fn.length,
fallback: fn
});
}
/**
* Creates a generic function from validated options.
* @private
* @param {Object} opt - the validated option object of the generic function.
* @param {string} [opt.name = ''] - the name of the generic function.
* @param {number} [opt.length = 0] - the length of the generic function.
* @param {Function} [opt.fallback = throwingHandler] - the fallback handler to be used when no other handler matches the arguments.
* @return {Function} a generic function.
**/
function createValidated({name, length, fallback}) {
const dispatcher = new Dispatcher();
const generic = function (...args) {
const handler = dispatcher.getHandler(args.slice(0, length)) || fallback || throwingHandler(generic);
return handler(...args);
};
generic.when = function when(predicates, handler) {
if (!isArray(predicates) || predicates.length != length) {
throw notAnArrayOfPredicatesError(predicates, length);
}
let pos = 0;
for (const predicate of predicates) {
if (!isFunction(predicate)) {
throw notAPredicateError(predicate, pos);
}
++pos;
}
if (!isFunction(handler)) {
throw handlerNotAFunctionError(handler);
}
dispatcher.setHandler(predicates, handler);
};
Reflect.defineProperty(generic, 'name', {value: name});
Reflect.defineProperty(generic, 'length', {value: length});
return generic;
}
/**
* Creates a handler that always throws, regardless of its arguments.
* @private
* @param {Function} generic - a generic function.
* @return {Function} a handler that always throws.
**/
function throwingHandler(generic) {
return function (...args) {
throw new NoMatchingError(generic, args);
};
}
/**
* An error for signaling when no matching could be found.
**/
class NoMatchingError extends Error {
/**
* Creates a "no matching" error.
* @param {function} generic - the generic function for which no matching was found.
* @param {Array.<*>} args - the arguments that did not match.
**/
constructor(generic, args) {
super(`Generic function "${generic.name}" cannot be applied to arguments ${inspect(args)}`);
/* istanbul ignore else */
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.generic = generic;
this.args = args;
}
}
/**
* Constructs a type error to signal that what provided as fallback handler is not a function.
* @private
* @param {*} fallback - what has been erroneusly passed as the fallback handler.
* @return {TypeError} a type error with an informing error message.
**/
function fallbackNotAFunctionError(fallback) {
const msg = `Expecting the fallback handler to be a function, got ${inspect(fallback)} instead`;
return new TypeError(msg);
}
/**
* Constructs a type error to signal that there was an attempt to promote
* something other than a function.
* @private
* @param {*} fn - what has been erroneusly passed as a function.
* @return {TypeError} a type error with an informing error message.
**/
function promotingNotAFunctionError(fn) {
const msg = `Expecting a function, got ${inspect(fn)} instead`;
return new TypeError(msg);
}
/**
* Constructs a type error to signal that what provided as handler is not a function.
* @private
* @param {*} handler - what has been erroneusly passed as an handler.
* @return {TypeError} a type error with an informing error message.
**/
function handlerNotAFunctionError(handler) {
const msg = `Expecting a function as second argument, got ${inspect(handler)} instead`;
return new TypeError(msg);
}
/**
* Constructs a type error to signal that what provided is not an array of predicates.
* @private
* @param {*} predicates - what has been erroneusly passed as an array of predicates.
* @param {number} length - the expect length of the predicate array.
* @return {TypeError} a type error with an informing error message.
**/
function notAnArrayOfPredicatesError(predicates, length) {
const msg = `Expecting an array of predicates of length ${length} as first argument, got ${inspect(predicates)} instead`;
return new TypeError(msg);
}
/**
* Constructs a type error to signal that what provided is not a predicate.
* @private
* @param {*} predicate - what has been erroneusly passed as part of an array of predicates.
* @param {number} pos - the position in the array of predicates.
* @return {TypeError} a type error with an informing error message.
**/
function notAPredicateError(predicate, pos) {
const msg = `Expecting a predicate function at position ${pos}, got ${inspect(predicate)} instead`;
return new TypeError(msg);
}
/**
* Checks if a value is a function.
* @private
* @param {*} arg - the argument to check.
* @return {boolean} whether the argument is in fact a function.
**/
function isFunction(arg) {
return typeof arg === 'function';
}
module.exports = {
create,
of,
NoMatchingError
};