function SubTypeError(name) { const cls = function () { const instance = Error(...arguments); Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); Object.defineProperty(instance, "name", { value: name, writable: false, }); return instance; }; cls.prototype = Object.create(Error); return cls; } export const NoNextMethodError = SubTypeError("NoNextMethodError"); export const NoApplicableMethodError = SubTypeError("NoApplicableMethodError"); export const NoPrimaryMethodError = SubTypeError("NoPrimaryMethodError"); export class UnhandledObjType extends Error { /** * @param {string} objType */ constructor(objType, ...params) { super(...params); this.objType = objType; } toString() { return `[${this.name}: unhandled objType: ${this.objType}]`; } } const before_qualifier = Symbol.for("before"); const after_qualifier = Symbol.for("after"); const around_qualifier = Symbol.for("around"); /** * The base prototype for a method. */ const Method = { lambda_list: [], qualifiers: [], specializers: [], body: () => {}, generic_function: null, }; /** @lends GenericFunction.prototype */ let genfun_prototype = { name: "(placeholder)", lambda_list: [], methods: [], method(qualifiers, specializers, body) { ensure_method(this, this.lambda_list, qualifiers, specializers, body); return this; }, /** * Add a primary method to the generic function. In the context of * the body, `this` has the available functions `call_next_method` * and the property `next_method_p` which allow delegating to other * implementations of the generic function. Only one of these will * be executed, unless `call_next_method` is called. * * @param {Specializer[]} specializers the specializers controlling * dispatch to this method * @param {Function} body the implementation of the method * @returns GenericFunction */ primary(specializers, body) { return this.method([], specializers, body); }, /** * Add a before method to the generic function. Every before method * is runs before the primary method and no before method can * influence the return value of the generic function. * * @param {Specializer[]} specializers the specializers controlling dispatch to this method * @param {Function} body the implementation of the method * @returns GenericFunction */ before(specializers, body) { return this.method([before_qualifier], specializers, body); }, /** * Add a after method to the generic function. Every after method * is runs after the primary method and no after method can * influence the return value of the generic function. * * @param {Specializer[]} specializers the specializers controlling dispatch to this method * @param {Function} body the implementation of the method * @returns GenericFunction */ after(specializers, body) { return this.method([after_qualifier], specializers, body); }, /** * Add a around method to the generic function. In the context of * the body, `this` has the available functions `call_next_method` * and the property `next_method_p` which allow delegating to other * implementations of the generic function. A generic function with * only around methods cannot be called. However, an around method * can skip the invocation of the primary method by not calling * `call_next_method` in its body. An around method can also * modify the results of the primary method and/or the arguments to * the primary method. Note that changing the arguments to the * primary method in a way that violates the specializers is * unsupported and may have surprising consequences. * * @param {Specializer[]} specializers the specializers controlling dispatch to this method * @param {Function} body the implementation of the method * @returns GenericFunction */ around(specializers, body) { return this.method([around_qualifier], specializers, body); }, get fn() { const gf = this; const lambda = function () { return apply_generic_function(gf, [].slice.call(arguments)); }.bind(gf); return Object.defineProperties(lambda, { name: { value: gf.name }, lambda_list: { value: gf.lambda_list }, gf: { value: gf }, }); }, }; /** * @class * @param {string} name * @param {string[]} lambda_list * @property {Method[]} methods */ function GenericFunction(name, lambda_list) { if (!(this instanceof GenericFunction)) { return new GenericFunction(...arguments); } this.name = name; this.lambda_list = lambda_list; this.methods = []; } GenericFunction.prototype = Object.create(genfun_prototype); /** * The main entrypoint to the library. Code like this constructs a new * generic function named `foo`: * * ```js * const foo = defgeneric('foo', 'arg1', 'arg2', 'arg3'); * ``` * * To add methods to the generic function, you grab the generic * function reference and call the relevant methods (arrow functions * may be used, unless you want access to `call_next_method`): * * ```js * foo.primary([String, Object, Object], (arg1, arg2, arg3) => { ... }); * foo.around([String, Object, Object], function (arg1, arg2, arg3) { * if (arg1 !== 'a') { * return this.call_next_method(); * } * }); * ``` * * Note that these names mostly exist for introspection * purposes. These were used in custom formatters, but Chrome removed * that functionality. * */ export function defgeneric(name, ...argument_names) { return GenericFunction(name, argument_names); } // let method_prototype = { // lambda_list: [], // qualifiers: [], // specializrs: [], // body() { throw new Error('Unimplemented'); }, // environment: {}, // generic_function: {}, // }; /** * @class * @extends Method * @param {string[]} lambda_list * @param {Symbol[]} qualifiers * @param {Specializer | Object} specializers * @param {Function} body */ export function StandardMethod(lambda_list, qualifiers, specializers, body) { if (!(this instanceof StandardMethod)) { return new StandardMethod(...arguments); } this.lambda_list = lambda_list; this.qualifiers = qualifiers; this.specializers = specializers; this.body = body; this.generic_function = null; } StandardMethod.prototype = Object.create(Method); function ensure_method(gf /*, lambda_list, qualifiers, specializers, body*/) { let new_method = StandardMethod(...[].slice.call(arguments, 1)); add_method(gf, new_method); return new_method; } function add_method(gf, method) { method.generic_function = gf; gf.methods.push(method); return method; } // function classes_of(args) { // return args.map(Object.getPrototypeOf); // } const required_portion = x => x; function apply_generic_function(gf, args) { let applicable_methods = compute_applicable_methods_using_classes( gf, required_portion(args) ); if (applicable_methods.length === 0) { throw new NoApplicableMethodError( `no applicable methods for gf ${gf.name} with args ${JSON.stringify( args )}` ); } else { return apply_methods(gf, args, applicable_methods); } } function method_more_specific_p(m1, m2 /*, required_classes*/) { const m1specializers = m1.specializers; const m2specializers = m2.specializers; let result = null; for (let [spec1, spec2] of m1specializers.map((el, idx) => [ el, m2specializers[idx], ])) { if (spec1 !== spec2) { result = sub_specializer_p(spec1, spec2); break; } } return result; } export function sub_specializer_p(c1, c2) { let result = false; if (c1 instanceof Specializer) { result = c1.super_of(c2); } else if (c2 instanceof Specializer) { result = !c2.super_of(c1); } else if (c1.prototype !== undefined && c2.prototype !== undefined) { result = Object.isPrototypeOf.call(c1.prototype, c2.prototype); } return result; } const idS = Symbol.for("id"); Object.prototype[idS] = function () { return this; }; export function Specializer() {} Specializer.prototype = { matches(_obj) { return false; }, super_of(_obj) { return false; }, }; function isSuperset(superset, subset) { return ( superset.size > subset.size && Array.from(subset).every(superset.has.bind(superset)) ); } const matchShape = defgeneric("matchShape", "shape", "value") .primary([Array], ([name, dflt], v) => dflt !== undefined && v[name] === dflt) .primary([String], (name, v) => v[name] !== undefined).fn; export const extractKey = defgeneric("extractKey", "key") .primary([Array], ([name, _]) => name) .primary([String], name => name).fn; export function Shape(...keys) { if (!(this instanceof Shape)) { return new Shape(...keys); } this.keys = new Set(keys); } Shape.prototype = Object.assign(new Specializer(), { matches(obj) { return Array.from(this.keys).every(key => matchShape(key, obj)); }, super_of(spec) { // this is the super of spec // if this.keys is a subset of spec.keys // and if this.keys != spec.keys if (!(spec instanceof Shape)) { const specKeys = spec && new Set(Object.getOwnPropertyNames(spec)); return !!specKeys && isSuperset(specKeys, this.keys); } else { return isSuperset(spec.keys, this.keys); } }, }); export function Eql(val) { if (!(this instanceof Eql)) { return new Eql(val); } this.val = val; } Eql.prototype = Object.assign(new Specializer(), { toString() { return `AEql(${this.val})`; }, matches(other) { return this.val === other; }, super_of() { return false; }, }); // function trace(fun) { // return function (...args) { // console.log(fun, `args are: thsds`, this, 'others', args); // const result = fun.apply(this, args); // console.log(`result`, result); // return result; // } // } export function matches_specializer(obj, specializer) { let objType = typeof obj; let specializer_proto = specializer && specializer.prototype; let result = obj === specializer_proto; if (specializer instanceof Specializer) { result = specializer.matches(obj); } else if (obj === null && (obj === specializer || specializer === Object)) { result = true; } else if (specializer && specializer.prototype !== undefined) { if (objType === "object") { if (!result) { result = Object.isPrototypeOf.call(specializer_proto, obj); } } else if (objType === "number") { result = matches_specializer(Number.prototype, specializer); } else if (objType === "boolean") { result = matches_specializer(Boolean.prototype, specializer); } else if (objType === "string") { result = matches_specializer(String.prototype, specializer); } else if (objType === "symbol") { result = matches_specializer(Symbol.prototype, specializer); } else if (objType === "undefined") { result = specializer === Object || obj === specializer; } else if (objType === "bigint") { result = matches_specializer(BigInt.prototype, specializer); } else { throw new UnhandledObjType(objType); } } return result; } /** * @param {GenericFunction} gf */ function compute_applicable_methods_using_classes(gf, required_classes) { const applicable_methods = gf.methods.filter(method => method.specializers.every((specializer, idx) => matches_specializer(required_classes[idx], specializer) ) ); applicable_methods.sort((a, b) => { let result = 0; if (method_more_specific_p(a, b)) { result = 1; } if (method_more_specific_p(b, a)) { result = -1; } return result; }); return applicable_methods; } /** * @param {any[]} a1 * @param {any[]} a2 */ function arr_eq(a1, a2) { if (a1.length !== a2.length) { return false; } else { for (let x = 0; x < a1.length; x++) { if (a1[x] instanceof Array && a2[x] instanceof Array) { if (!arr_eq(a1[x], a2[x])) { return false; } } else if (a1[x] !== a2[x]) { return false; } else if ( Object.hasOwnProperty.call(a1[x], "equals") && !a1[x].equals(a2[x]) ) { return false; } else if ( Object.hasOwnProperty.call(a2[x], "equals") && !a2[x].equals(a1[x]) ) { return false; } } return true; } } // function set_eq(a1, a2) { // if (a1.length !== a2.length) { // return false; // } else { // let result = true; // for (let elem of a1) { // result = result && a2.has(elem); // if (!result) break; // } // return result; // } // } const primary_method_p = method => method instanceof WrappedMethod || method.qualifiers.length === 0; const before_method_p = method => !(method instanceof WrappedMethod) && arr_eq(method.qualifiers, [before_qualifier]); const after_method_p = method => !(method instanceof WrappedMethod) && arr_eq(method.qualifiers, [after_qualifier]); const around_method_p = method => !(method instanceof WrappedMethod) && arr_eq(method.qualifiers, [around_qualifier]); function WrappedMethod(continuation) { this.continuation = continuation; } /** * @param {GenericFunction} gf * @param {any[]} args * @param {Method[]} applicable_methods */ function apply_methods(gf, args, applicable_methods) { const primaries = applicable_methods.filter(primary_method_p); const befores = applicable_methods.filter(before_method_p); const arounds = applicable_methods.filter(around_method_p); const afters = applicable_methods.filter(after_method_p); afters.reverse(); const main_call = Object.defineProperty( function () { if (primaries.length === 0) { throw new NoPrimaryMethodError(`No primary method for ${gf.name}`); } for (let before of befores) { apply_method(before, args, []); } try { return apply_method(primaries[0], args, primaries.slice(1)); } finally { for (let after of afters) { apply_method(after, args, []); } } }, "name", { value: `main_call_${gf.name}` } ); if (arounds.length === 0) { return main_call(); } else { const wrapped_main_call = new WrappedMethod(main_call); const next_methods = arounds.slice(1).concat([wrapped_main_call]); return apply_method(arounds[0], args, next_methods); } } /** * @param {Method} method * @param {any[]} args * @param {Method[]} next_methods */ function apply_method(method, args, next_methods) { const method_context = { call_next_method(...cnm_args) { if (next_methods.length === 0) { throw new NoNextMethodError( `no next method for genfun ${method.generic_function.name}` ); } return method instanceof WrappedMethod ? method.continuation() : apply_methods( method.generic_function, cnm_args.length > 0 ? cnm_args : args, next_methods ); }, get next_method_p() { return next_methods.length !== 0; }, }; return method.body ? method.body.bind(method_context)(...args) : method.continuation(); }