From 814f66000024534fa6d8585007649d66485df44f Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 16:24:20 -0700 Subject: [PATCH] feat(Chain): add computation pipelines like mathjs Also adds a `mean()` operation so there will be at least one operation that takes only a rest parameter, to exercise the ban on splitting such a parameter between the stored value and new arguments. Adds various tests of chains. Resolves #32. --- src/core/Chain.mjs | 70 +++++++++++++++++++++++++++++++++++ src/core/PocomathInstance.mjs | 8 ++++ src/generic/arithmetic.mjs | 1 + src/generic/mean.mjs | 3 ++ src/generic/multiply.mjs | 13 ------- test/_pocomath.mjs | 11 ++++++ test/custom.mjs | 8 +++- 7 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 src/core/Chain.mjs create mode 100644 src/generic/mean.mjs delete mode 100644 src/generic/multiply.mjs diff --git a/src/core/Chain.mjs b/src/core/Chain.mjs new file mode 100644 index 0000000..11cb636 --- /dev/null +++ b/src/core/Chain.mjs @@ -0,0 +1,70 @@ +/* An object that holds a value and a reference to a PocomathInstance for + * applying operations to that value. Since the operations need to be wrapped, + * that instance is supposed to provide a place where wrapped operations can + * be stored, known as the repository. + */ +class Chain { + constructor(value, instance, repository) { + this.value = value + this.instance = instance + this.repository = repository + if (!('_wrapped' in this.repository)) { + this.repository._wrapped = {} + } + } + + /* This is the wrapper for func which calls it with the chain's + * current value inserted as the first argument. + */ + _chainify(func, typed) { + return function () { + // Here `this` is the proxied Chain instance + if (arguments.length === 0) { + this.value = func(this.value) + } else { + const args = [this.value, ...arguments] + if (typed.isTypedFunction(func)) { + const sigObject = typed.resolve(func, args) + if (sigObject.params.length === 1) { + throw new Error( + `chain function ${func.name} attempting to split a rest` + + 'parameter between chain value and other arguments') + } + this.value = sigObject.implementation.apply(func, args) + } else { + this.value = func.apply(func, args) + } + } + return this + } + } +} + +export function makeChain(value, instance, repository) { + const chainObj = new Chain(value, instance, repository) + /* Rather than using the chainObj directly, we Proxy it to + * ensure we only get back wrapped, current methods of the instance. + */ + return new Proxy(chainObj, { + get: (target, property) => { + if (property === 'value') return target.value + if (!(property in target.instance)) { + throw new SyntaxError(`Unknown operation ${property}`) + } + if (property.charAt(0) === '_') { + throw new SyntaxError(`No access to private ${property}`) + } + const curval = target.instance[property] + if (typeof curval !== 'function') { + throw new SyntaxError( + `Property ${property} does not designate an operation`) + } + if (curval != target.repository._wrapped[property]) { + target.repository._wrapped[property] = curval + target.repository[property] = target._chainify( + curval, target.instance._typed) + } + return target.repository[property] + } + }) +} diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 4858a54..c1cee4a 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,6 +1,7 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' import dependencyExtractor from './dependencyExtractor.mjs' +import {makeChain} from './Chain.mjs' import {subsetOfKeys, typesOfSignature} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -21,6 +22,7 @@ export default class PocomathInstance { * must be added to this list. */ static reserved = new Set([ + 'chain', 'config', 'importDependencies', 'install', @@ -64,6 +66,7 @@ export default class PocomathInstance { return true // successful } }) + this._chainRepository = {} // place to store chainified functions } /** @@ -164,6 +167,11 @@ export default class PocomathInstance { return result } + /* Return a chain object for this instance with a given value: */ + chain(value) { + return makeChain(value, this, this._chainRepository) + } + _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { if (type === 'any' || this._templateParam(type)) continue diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index 7576dbe..f6abd23 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -4,6 +4,7 @@ export * from './Types/generic.mjs' export const add = reducingOperation export {lcm} from './lcm.mjs' +export {mean} from './mean.mjs' export {mod} from './mod.mjs' export const multiply = reducingOperation export {divide} from './divide.mjs' diff --git a/src/generic/mean.mjs b/src/generic/mean.mjs new file mode 100644 index 0000000..d12c21b --- /dev/null +++ b/src/generic/mean.mjs @@ -0,0 +1,3 @@ +export const mean = { + '...any': ({add, divide}) => args => divide(add(...args), args.length) +} diff --git a/src/generic/multiply.mjs b/src/generic/multiply.mjs deleted file mode 100644 index 63d196a..0000000 --- a/src/generic/multiply.mjs +++ /dev/null @@ -1,13 +0,0 @@ -export * from './Types/generic.mjs' - -export const multiply = { - 'undefined': () => u => u, - 'undefined,...any': () => (u, rest) => u, - any: () => x => x, - 'any,undefined': () => (x, u) => u, - 'any,any,...any': ({self}) => (a,b,rest) => { - const later = [b, ...rest] - return later.reduce((x,y) => self(x,y), a) - } -} - diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 3e6f2fe..222fbd6 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -82,4 +82,15 @@ describe('The default full pocomath instance "math"', () => { math.complex(11n, -4n)) assert.strictEqual(math.negate(math.complex(3n, 8n)).im, -8n) }) + + it('creates chains', () => { + const mychain = math.chain(7).negate() + assert.strictEqual(mychain.value, -7) + mychain.add(23).sqrt().lcm(10) + assert.strictEqual(mychain.value, 20) + assert.strictEqual(math.mean(3,4,5), 4) + assert.throws(() => math.chain(3).mean(4,5), /chain function.*split/) + assert.throws(() => math.chain(3).foo(), /Unknown operation/) + }) + }) diff --git a/test/custom.mjs b/test/custom.mjs index ba4c713..9fb66b1 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -17,7 +17,9 @@ describe('A custom instance', () => { it("works when partially assembled", () => { bw.install(complex) // Not much we can call without any number types: - assert.deepStrictEqual(bw.complex(0, 3), {re: 0, im: 3}) + const i3 = {re: 0, im: 3} + assert.deepStrictEqual(bw.complex(0, 3), i3) + assert.deepStrictEqual(bw.chain(0).complex(3).value, i3) // Don't have a way to negate things, for example: assert.throws(() => bw.negate(2), TypeError) }) @@ -40,6 +42,7 @@ describe('A custom instance', () => { assert.strictEqual(pm.subtract(5, 10), -5) assert.strictEqual(pm.floor(3.7), 3) assert.throws(() => pm.floor(10n), TypeError) + assert.strictEqual(pm.chain(5).add(7).value, 12) pm.install(complexAdd) pm.install(complexNegate) pm.install(complexComplex) @@ -52,6 +55,9 @@ describe('A custom instance', () => { assert.deepStrictEqual( pm.floor(math.complex(1.9, 0)), math.complex(1)) + // And the chain functions refresh themselves: + assert.deepStrictEqual( + pm.chain(5).add(pm.chain(0).complex(7).value).value, math.complex(5,7)) }) it("can defer definition of (even used) types", () => {