git.fiddlerwoaroof.com
src/genfuns.js
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
 }