0fb7ba9b |
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;
}
|
406b5587 |
|
0fb7ba9b |
export const NoNextMethodError = SubTypeError("NoNextMethodError");
export const NoApplicableMethodError = SubTypeError("NoApplicableMethodError");
export const NoPrimaryMethodError = SubTypeError("NoPrimaryMethodError");
|
406b5587 |
|
42c8c155 |
export class UnhandledObjType extends Error {
/**
* @param {string} objType
*/
constructor(objType, ...params) {
super(...params);
this.objType = objType;
}
toString() {
return `[${this.name}: unhandled objType: ${this.objType}]`;
}
}
|
94743407 |
const before_qualifier = Symbol.for("before");
const after_qualifier = Symbol.for("after");
const around_qualifier = Symbol.for("around");
|
5ff4d201 |
/**
* The base prototype for a method.
*/
|
94743407 |
const Method = {
lambda_list: [],
qualifiers: [],
specializers: [],
body: () => {},
|
cc733b08 |
generic_function: null,
|
94743407 |
};
|
94944213 |
|
5ff4d201 |
/** @lends GenericFunction.prototype */
|
94944213 |
let genfun_prototype = {
|
94743407 |
name: "(placeholder)",
lambda_list: [],
methods: [],
method(qualifiers, specializers, body) {
ensure_method(this, this.lambda_list, qualifiers, specializers, body);
return this;
},
|
5ff4d201 |
/**
* 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
*/
|
94743407 |
primary(specializers, body) {
return this.method([], specializers, body);
},
|
5ff4d201 |
/**
* 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
*/
|
94743407 |
before(specializers, body) {
return this.method([before_qualifier], specializers, body);
},
|
5ff4d201 |
/**
* 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
*/
|
94743407 |
after(specializers, body) {
return this.method([after_qualifier], specializers, body);
},
|
5ff4d201 |
/**
* 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
*/
|
94743407 |
around(specializers, body) {
return this.method([around_qualifier], specializers, body);
},
get fn() {
const gf = this;
|
cc733b08 |
const lambda = function () {
|
94743407 |
return apply_generic_function(gf, [].slice.call(arguments));
}.bind(gf);
return Object.defineProperties(lambda, {
name: { value: gf.name },
lambda_list: { value: gf.lambda_list },
|
cc733b08 |
gf: { value: gf },
|
94743407 |
});
|
cc733b08 |
},
|
94944213 |
};
|
94743407 |
/**
* @class
* @param {string} name
* @param {string[]} lambda_list
* @property {Method[]} methods
*/
|
94944213 |
function GenericFunction(name, lambda_list) {
|
94743407 |
if (!(this instanceof GenericFunction)) {
return new GenericFunction(...arguments);
}
|
94944213 |
|
94743407 |
this.name = name;
this.lambda_list = lambda_list;
this.methods = [];
|
94944213 |
}
GenericFunction.prototype = Object.create(genfun_prototype);
|
5ff4d201 |
/**
* 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.
*
*/
|
94944213 |
export function defgeneric(name, ...argument_names) {
|
94743407 |
return GenericFunction(name, argument_names);
|
94944213 |
}
|
a00b5a50 |
// let method_prototype = {
// lambda_list: [],
// qualifiers: [],
// specializrs: [],
// body() { throw new Error('Unimplemented'); },
// environment: {},
// generic_function: {},
// };
|
94944213 |
|
94743407 |
/**
* @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;
|
94944213 |
}
|
94743407 |
StandardMethod.prototype = Object.create(Method);
|
94944213 |
|
94743407 |
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;
|
94944213 |
}
function add_method(gf, method) {
|
94743407 |
method.generic_function = gf;
gf.methods.push(method);
return method;
|
94944213 |
}
|
a00b5a50 |
// function classes_of(args) {
// return args.map(Object.getPrototypeOf);
// }
|
94944213 |
const required_portion = x => x;
function apply_generic_function(gf, args) {
|
94743407 |
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);
}
|
94944213 |
}
|
94743407 |
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,
|
cc733b08 |
m2specializers[idx],
|
94743407 |
])) {
if (spec1 !== spec2) {
result = sub_specializer_p(spec1, spec2);
break;
|
94944213 |
}
|
94743407 |
}
|
406b5587 |
|
94743407 |
return result;
|
94944213 |
}
|
406b5587 |
export function sub_specializer_p(c1, c2) {
|
94743407 |
let result = false;
if (c1 instanceof Specializer) {
result = c1.super_of(c2);
|
b852ec93 |
} else if (c2 instanceof Specializer) {
result = !c2.super_of(c1);
|
94743407 |
} else if (c1.prototype !== undefined && c2.prototype !== undefined) {
result = Object.isPrototypeOf.call(c1.prototype, c2.prototype);
}
return result;
|
94944213 |
}
|
94743407 |
const idS = Symbol.for("id");
|
cc733b08 |
Object.prototype[idS] = function () {
|
94743407 |
return this;
};
|
94944213 |
|
94743407 |
export function Specializer() {}
|
406b5587 |
Specializer.prototype = {
|
94743407 |
matches(_obj) {
return false;
},
super_of(_obj) {
return false;
|
cc733b08 |
},
|
94743407 |
};
|
406b5587 |
function isSuperset(superset, subset) {
|
94743407 |
return (
superset.size > subset.size &&
Array.from(subset).every(superset.has.bind(superset))
);
|
406b5587 |
}
|
a00b5a50 |
const matchShape = defgeneric("matchShape", "shape", "value")
|
94743407 |
.primary([Array], ([name, dflt], v) => dflt !== undefined && v[name] === dflt)
.primary([String], (name, v) => v[name] !== undefined).fn;
|
a00b5a50 |
export const extractKey = defgeneric("extractKey", "key")
|
94743407 |
.primary([Array], ([name, _]) => name)
.primary([String], name => name).fn;
|
a00b5a50 |
|
406b5587 |
export function Shape(...keys) {
|
94743407 |
if (!(this instanceof Shape)) {
return new Shape(...keys);
}
this.keys = new Set(keys);
|
406b5587 |
}
Shape.prototype = Object.assign(new Specializer(), {
|
94743407 |
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);
|
406b5587 |
}
|
cc733b08 |
},
|
406b5587 |
});
|
159b36ec |
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;
},
|
2c54c081 |
super_of() {
|
159b36ec |
return false;
},
});
|
a00b5a50 |
// 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;
// }
// }
|
406b5587 |
export function matches_specializer(obj, specializer) {
|
94743407 |
let objType = typeof obj;
let specializer_proto = specializer && specializer.prototype;
let result = obj === specializer_proto;
|
32f91b0a |
if (specializer instanceof Specializer) {
result = specializer.matches(obj);
} else if (obj === null && (obj === specializer || specializer === Object)) {
|
94743407 |
result = true;
} else if (specializer && specializer.prototype !== undefined) {
|
42c8c155 |
if (objType === "object") {
if (!result) {
result = Object.isPrototypeOf.call(specializer_proto, obj);
}
|
94743407 |
} else if (objType === "number") {
|
42c8c155 |
result = matches_specializer(Number.prototype, specializer);
} else if (objType === "boolean") {
result = matches_specializer(Boolean.prototype, specializer);
|
94743407 |
} else if (objType === "string") {
|
42c8c155 |
result = matches_specializer(String.prototype, specializer);
} else if (objType === "symbol") {
result = matches_specializer(Symbol.prototype, specializer);
} else if (objType === "undefined") {
|
32f91b0a |
result = specializer === Object || obj === specializer;
|
42c8c155 |
} else if (objType === "bigint") {
result = matches_specializer(BigInt.prototype, specializer);
} else {
throw new UnhandledObjType(objType);
|
94944213 |
}
|
94743407 |
}
|
94944213 |
|
94743407 |
return result;
|
94944213 |
}
|
94743407 |
/**
* @param {GenericFunction} gf
*/
|
94944213 |
function compute_applicable_methods_using_classes(gf, required_classes) {
|
94743407 |
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;
}
|
406b5587 |
|
94743407 |
return result;
});
|
94944213 |
|
94743407 |
return applicable_methods;
|
94944213 |
}
|
94743407 |
/**
* @param {any[]} a1
* @param {any[]} a2
*/
|
94944213 |
function arr_eq(a1, a2) {
|
94743407 |
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;
|
94944213 |
}
|
94743407 |
} 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;
}
|
94944213 |
}
|
94743407 |
return true;
}
|
94944213 |
}
|
a00b5a50 |
// 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;
// }
// }
|
406b5587 |
|
94743407 |
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]);
|
94944213 |
function WrappedMethod(continuation) {
|
94743407 |
this.continuation = continuation;
|
94944213 |
}
|
94743407 |
/**
* @param {GenericFunction} gf
* @param {any[]} args
* @param {Method[]} applicable_methods
*/
|
94944213 |
function apply_methods(gf, args, applicable_methods) {
|
94743407 |
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(
|
cc733b08 |
function () {
|
94743407 |
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);
}
|
94944213 |
}
|
94743407 |
/**
* @param {Method} method
* @param {any[]} args
* @param {Method[]} next_methods
*/
|
94944213 |
function apply_method(method, args, next_methods) {
|
94743407 |
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;
|
cc733b08 |
},
|
94743407 |
};
|
94944213 |
|
94743407 |
return method.body
? method.body.bind(method_context)(...args)
: method.continuation();
|
94944213 |
}
|