From 9fb3aa29590ec2c4abd6a9747e82f0c91af327d1 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 23 Jul 2022 09:55:02 -0700 Subject: [PATCH] feat: Switch to function-based specification of dependencies Allows dependencies to be economically expressed and used. For example, see the new definition of subtract. Credit for the basic idea goes to James Drew, see https://stackoverflow.com/a/41525264 Resolves #21. --- src/bigint/add.mjs | 3 +- src/bigint/negate.mjs | 3 +- src/complex/add.mjs | 13 ++- src/complex/complex.mjs | 4 +- src/complex/negate.mjs | 7 +- src/core/PocomathInstance.mjs | 133 +++++++++++++---------------- src/core/dependencyExtractor.mjs | 9 ++ src/generic/subtract.mjs | 4 +- src/number/add.mjs | 2 +- src/number/negate.mjs | 2 +- test/_pocomath.mjs | 2 +- test/core/_PocomathInstance.mjs | 2 +- test/core/_dependencyExtractor.mjs | 22 +++++ 13 files changed, 104 insertions(+), 102 deletions(-) create mode 100644 src/core/dependencyExtractor.mjs create mode 100644 test/core/_dependencyExtractor.mjs diff --git a/src/bigint/add.mjs b/src/bigint/add.mjs index dfff057..cc1f48e 100644 --- a/src/bigint/add.mjs +++ b/src/bigint/add.mjs @@ -1,6 +1,5 @@ -import {use} from '../core/PocomathInstance.mjs' export {Types} from './Types/bigint.mjs' export const add = { - '...bigint': use([], addends => addends.reduce((x,y) => x+y, 0n)) + '...bigint': () => addends => addends.reduce((x,y) => x+y, 0n) } diff --git a/src/bigint/negate.mjs b/src/bigint/negate.mjs index eed50bf..874a377 100644 --- a/src/bigint/negate.mjs +++ b/src/bigint/negate.mjs @@ -1,4 +1,3 @@ -import {use} from '../core/PocomathInstance.mjs' export {Types} from './Types/bigint.mjs' -export const negate = {bigint: use([], b => -b)} +export const negate = {bigint: () => b => -b} diff --git a/src/complex/add.mjs b/src/complex/add.mjs index d44d3fd..27374b6 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,13 +1,10 @@ export {Types} from './Types/Complex.mjs' export const add = { - '...Complex': { - uses: ['self'], - does: ref => addends => { - if (addends.length === 0) return {re:0, im:0} - const seed = addends.shift() - return addends.reduce((w,z) => - ({re: ref.self(w.re, z.re), im: ref.self(w.im, z.im)}), seed) - } + '...Complex': ({self}) => addends => { + if (addends.length === 0) return {re:0, im:0} + const seed = addends.shift() + return addends.reduce( + (w,z) => ({re: self(w.re, z.re), im: self(w.im, z.im)}), seed) } } diff --git a/src/complex/complex.mjs b/src/complex/complex.mjs index 9a5cee9..5cbbced 100644 --- a/src/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -5,7 +5,7 @@ export const complex = { * have a numeric/scalar type, e.g. by implementing subtypes in * typed-function */ - 'any, any': {does: (x, y) => ({re: x, im: y})}, + 'any, any': () => (x, y) => ({re: x, im: y}), /* Take advantage of conversions in typed-function */ - Complex: {does: z => z} + Complex: () => z => z } diff --git a/src/complex/negate.mjs b/src/complex/negate.mjs index 9cb21c6..45b9840 100644 --- a/src/complex/negate.mjs +++ b/src/complex/negate.mjs @@ -1,10 +1,5 @@ export {Types} from './Types/Complex.mjs' export const negate = { - Complex: { - uses: ['self'], - does: ref => z => { - return {re: ref.self(z.re), im: ref.self(z.im)} - } - } + Complex: ({self}) => z => ({re: self(z.re), im: self(z.im)}) } diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 4fd2584..83a11f8 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,9 +1,6 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' - -export function use(dependencies, implementation) { - return [dependencies, implementation] -} +import dependencyExtractor from './dependencyExtractor.mjs' export default class PocomathInstance { /* Disallowed names for ops; beware, this is slightly non-DRY @@ -25,50 +22,35 @@ export default class PocomathInstance { /** * (Partially) define one or more operations of the instance: * - * @param {Object>} ops + * @param {Object implementation>>} ops * The only parameter ops gives the semantics of the operations to install. * The keys are operation names. The value for a key is an object - * mapping (typed-function) signature strings to specifications of - * of dependency lists and implementation functions. + * mapping each desired (typed-function) signature to a function taking + * a dependency object to an implementation. * - * A dependency list is a list of strings. Each string can either be the - * name of a function that the corresponding implementation has to call, - * or a specification of a particular signature of a function that it has - * to call, in the form 'FN(SIGNATURE)' [not implemented yet]. - * Note the function name can be the special value 'self' to indicate a - * recursive call to the given operation (either with or without a - * particular signature. + * For more detail, such functions should have the format + * ``` + * ({depA, depB, depC: aliasC, ...}) => (opArg1, opArg2) => + * ``` + * where the `depA`, `depB` etc. are the names of the + * operations this implementation depends on; those operations can + * then be referred to directly by the identifiers `depA` and `depB` + * in the code for the '`, or when an alias has been given + * as in the case of `depC`, by the identifier `aliasC`. + * Given an object that has these dependencies with these keys, the + * function returns a function taking the operation arguments to the + * desired result of the operation. * - * There are two cases for the implementation function. If the dependency - * list is empty, it should be a function taking the arguments specified - * by the signature and returning the value. Otherwise, it should be - * a function taking an object with the dependency lists as keys and the - * requested functions as values, to a function taking the arguments - * specified by the signature and returning the value. + * You can specify that an operation depends on itself by using the + * special dependency identifier 'self'. * - * There are various specifications currently allowed for the - * dependency list and implementation function: - * - * 1) Just a function. Then the dependency list is assumed to be empty. - * - * 2) A pair (= Array with two entries) of a dependency list and the - * implementation function. - * - * 3) An object whose property named 'does' gives the implementation - * function and whose property named 'uses', if present, gives the - * dependency list (which is assumed to be empty if the property is - * not present). - * - * 4) A call to the 'use' function exported from the this module, with - * first argument the dependencies and second argument the - * implementation. - * - * For a visual comparison of the options, this proof-of-concept uses - * option (1) when possible for the 'number' type, (3) for the 'Complex' - * type, (4) for the 'bigint' type, and (2) under any other circumstances. - * Likely a fleshed-out version of this scheme would settle on just one - * or two of these options or variants thereof, rather than providing so - * many different ones. + * You can specify that an implementation depends on just a specific + * signature of the given operation by suffixing the dependency name + * with the signature in parentheses, e.g. `add(number,number)` to + * refer to just adding two numbers. In this case, it is of course + * necessary to specify an alias to be able to refer to the supplied + * operation in the body of the implementation. [NOTE: this signature- + * specific reference is not yet implemented.] * * Note that the "operation" named `Types` is special: it gives * types that must be installed in the instance. In this case, the keys @@ -94,13 +76,8 @@ export default class PocomathInstance { /* Grab all of the known deps */ for (const func in this._imps) { if (func === 'Types') continue - for (const definition of Object.values(this._imps[func])) { - let deps = [] - if (Array.isArray(definition)) deps = definition[0] - else if (typeof definition === 'object') { - deps = definition.uses || deps - } - for (const dependency of deps) { + for (const {uses} of Object.values(this._imps[func])) { + for (const dependency of uses) { const depName = dependency.split('(',1)[0] if (doneSet.has(depName)) continue requiredSet.add(depName) @@ -137,14 +114,32 @@ export default class PocomathInstance { // new implementations, so set the op up to lazily recreate itself this._invalidate(name) const opImps = this._imps[name] - for (const signature in implementations) { + for (const [signature, does] of Object.entries(implementations)) { + if (name === 'Types') { + if (signature in opImps) { + if (does != opImps[signature]) { + throw newSyntaxError( + `Conflicting definitions of type ${signature}`) + } + } else { + opImps[signature] = does + } + continue + } if (signature in opImps) { - if (implementations[signature] === opImps[signature]) continue - throw new SyntaxError( - `Conflicting definitions of ${signature} for ${name}`) + if (does !== opImps[signature].does) { + throw new SyntaxError( + `Conflicting definitions of ${signature} for ${name}`) + } } else { - opImps[signature] = implementations[signature] - for (const dep of implementations[signature][0] || []) { + if (name === 'Types') { + opImps[signature] = does + continue + } + const uses = new Set() + does(dependencyExtractor(uses)) + opImps[signature] = {uses, does} + for (const dep of uses) { const depname = dep.split('(', 1)[0] if (depname === 'self') continue if (!(depname in this._affects)) { @@ -187,27 +182,13 @@ export default class PocomathInstance { } this._ensureTypes() const tf_imps = {} - for (const signature in imps) { - const specifier = imps[signature] - let deps = [] - let imp - if (typeof specifier === 'function') { - imp = specifier - } else if (Array.isArray(specifier)) { - [deps, imp] = specifier - } else if (typeof specifier === 'object') { - deps = specifier.uses || deps - imp = specifier.does - } else { - throw new SyntaxError( - `Cannot interpret signature definition ${specifier}`) - } - if (deps.length === 0) { - tf_imps[signature] = imp + for (const [signature, {uses, does}] of Object.entries(imps)) { + if (uses.length === 0) { + tf_imps[signature] = does() } else { const refs = {} let self_referential = false - for (const dep of deps) { + for (const dep of uses) { // TODO: handle signature-specific dependencies if (dep.includes('(')) { throw new Error('signature specific reference unimplemented') @@ -221,10 +202,10 @@ export default class PocomathInstance { if (self_referential) { tf_imps[signature] = this._typed.referToSelf(self => { refs.self = self - return imp(refs) + return does(refs) }) } else { - tf_imps[signature] = imp(refs) + tf_imps[signature] = does(refs) } } } diff --git a/src/core/dependencyExtractor.mjs b/src/core/dependencyExtractor.mjs new file mode 100644 index 0000000..fbf5611 --- /dev/null +++ b/src/core/dependencyExtractor.mjs @@ -0,0 +1,9 @@ +/* Call this with an empty Set object S, and it returns an entity E + * from which properties can be extracted, and at any time S will + * contain all of the property names that have been extracted from E. + */ +export default function dependencyExtractor(destinationSet) { + return new Proxy({}, { + get: (target, property) => { destinationSet.add(property) } + }) +} diff --git a/src/generic/subtract.mjs b/src/generic/subtract.mjs index 8a90697..96a27bf 100644 --- a/src/generic/subtract.mjs +++ b/src/generic/subtract.mjs @@ -1,3 +1,3 @@ export const subtract = { - 'any,any': [['add', 'negate'], ref => (x,y) => ref.add(x, ref.negate(y))] -} + 'any,any': ({add, negate}) => (x,y) => add(x, negate(y)) +} diff --git a/src/number/add.mjs b/src/number/add.mjs index d47b10e..e473c50 100644 --- a/src/number/add.mjs +++ b/src/number/add.mjs @@ -1,5 +1,5 @@ export {Types} from './Types/number.mjs' export const add = { - '...number': addends => addends.reduce((x,y) => x+y, 0), + '...number': () => addends => addends.reduce((x,y) => x+y, 0), } diff --git a/src/number/negate.mjs b/src/number/negate.mjs index bdfa823..387d905 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -1,3 +1,3 @@ export { Types } from './Types/number.mjs' -export const negate = {number: n => -n} +export const negate = {number: () => n => -n} diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 507d811..1ede574 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -19,7 +19,7 @@ describe('The default full pocomath instance "math"', () => { it('can be extended', () => { math.install({'add': { - '...string': [[], addends => addends.reduce((x,y) => x+y, '')] + '...string': () => addends => addends.reduce((x,y) => x+y, '') }}) assert.strictEqual(math.add('Kilroy',' is here'), 'Kilroy is here') }) diff --git a/test/core/_PocomathInstance.mjs b/test/core/_PocomathInstance.mjs index 5f64774..10cbb7a 100644 --- a/test/core/_PocomathInstance.mjs +++ b/test/core/_PocomathInstance.mjs @@ -4,7 +4,7 @@ import PocomathInstance from '../../src/core/PocomathInstance.mjs' const pi = new PocomathInstance('dummy') describe('PocomathInstance', () => { it('creates an instance that can define typed-functions', () => { - pi.install({add: {'any,any': [[], (a,b) => a+b]}}) + pi.install({add: {'any,any': () => (a,b) => a+b}}) assert.strictEqual(pi.add(2,2), 4) assert.strictEqual(pi.add('Kilroy', 17), 'Kilroy17') assert.strictEqual(pi.add(1, undefined), NaN) diff --git a/test/core/_dependencyExtractor.mjs b/test/core/_dependencyExtractor.mjs new file mode 100644 index 0000000..91e0e40 --- /dev/null +++ b/test/core/_dependencyExtractor.mjs @@ -0,0 +1,22 @@ +import assert from 'assert' +import dependencyExtractor from '../../src/core/dependencyExtractor.mjs' + +describe('dependencyExtractor', () => { + it('will record the keys of a destructuring function', () => { + const myfunc = ({a, 'b(x)': b, c: alias}) => 0 + const params = new Set() + myfunc(dependencyExtractor(params)) + assert.ok(params.has('a')) + assert.ok(params.has('b(x)')) + assert.ok(params.has('c')) + assert.ok(params.size === 3) + }) + + it('does not pick up anything from a regular function', () => { + const myfunc = arg => 0 + const params = new Set() + myfunc(dependencyExtractor(params)) + assert.ok(params.size === 0) + }) + +})