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.
This commit is contained in:
parent
46ae7f78ab
commit
814f660000
70
src/core/Chain.mjs
Normal file
70
src/core/Chain.mjs
Normal file
@ -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]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
/* Core of pocomath: create an instance */
|
/* Core of pocomath: create an instance */
|
||||||
import typed from 'typed-function'
|
import typed from 'typed-function'
|
||||||
import dependencyExtractor from './dependencyExtractor.mjs'
|
import dependencyExtractor from './dependencyExtractor.mjs'
|
||||||
|
import {makeChain} from './Chain.mjs'
|
||||||
import {subsetOfKeys, typesOfSignature} from './utils.mjs'
|
import {subsetOfKeys, typesOfSignature} from './utils.mjs'
|
||||||
|
|
||||||
const anySpec = {} // fixed dummy specification of 'any' type
|
const anySpec = {} // fixed dummy specification of 'any' type
|
||||||
@ -21,6 +22,7 @@ export default class PocomathInstance {
|
|||||||
* must be added to this list.
|
* must be added to this list.
|
||||||
*/
|
*/
|
||||||
static reserved = new Set([
|
static reserved = new Set([
|
||||||
|
'chain',
|
||||||
'config',
|
'config',
|
||||||
'importDependencies',
|
'importDependencies',
|
||||||
'install',
|
'install',
|
||||||
@ -64,6 +66,7 @@ export default class PocomathInstance {
|
|||||||
return true // successful
|
return true // successful
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this._chainRepository = {} // place to store chainified functions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,6 +167,11 @@ export default class PocomathInstance {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Return a chain object for this instance with a given value: */
|
||||||
|
chain(value) {
|
||||||
|
return makeChain(value, this, this._chainRepository)
|
||||||
|
}
|
||||||
|
|
||||||
_installInstance(other) {
|
_installInstance(other) {
|
||||||
for (const [type, spec] of Object.entries(other.Types)) {
|
for (const [type, spec] of Object.entries(other.Types)) {
|
||||||
if (type === 'any' || this._templateParam(type)) continue
|
if (type === 'any' || this._templateParam(type)) continue
|
||||||
|
@ -4,6 +4,7 @@ export * from './Types/generic.mjs'
|
|||||||
|
|
||||||
export const add = reducingOperation
|
export const add = reducingOperation
|
||||||
export {lcm} from './lcm.mjs'
|
export {lcm} from './lcm.mjs'
|
||||||
|
export {mean} from './mean.mjs'
|
||||||
export {mod} from './mod.mjs'
|
export {mod} from './mod.mjs'
|
||||||
export const multiply = reducingOperation
|
export const multiply = reducingOperation
|
||||||
export {divide} from './divide.mjs'
|
export {divide} from './divide.mjs'
|
||||||
|
3
src/generic/mean.mjs
Normal file
3
src/generic/mean.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const mean = {
|
||||||
|
'...any': ({add, divide}) => args => divide(add(...args), args.length)
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -82,4 +82,15 @@ describe('The default full pocomath instance "math"', () => {
|
|||||||
math.complex(11n, -4n))
|
math.complex(11n, -4n))
|
||||||
assert.strictEqual(math.negate(math.complex(3n, 8n)).im, -8n)
|
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/)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -17,7 +17,9 @@ describe('A custom instance', () => {
|
|||||||
it("works when partially assembled", () => {
|
it("works when partially assembled", () => {
|
||||||
bw.install(complex)
|
bw.install(complex)
|
||||||
// Not much we can call without any number types:
|
// 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:
|
// Don't have a way to negate things, for example:
|
||||||
assert.throws(() => bw.negate(2), TypeError)
|
assert.throws(() => bw.negate(2), TypeError)
|
||||||
})
|
})
|
||||||
@ -40,6 +42,7 @@ describe('A custom instance', () => {
|
|||||||
assert.strictEqual(pm.subtract(5, 10), -5)
|
assert.strictEqual(pm.subtract(5, 10), -5)
|
||||||
assert.strictEqual(pm.floor(3.7), 3)
|
assert.strictEqual(pm.floor(3.7), 3)
|
||||||
assert.throws(() => pm.floor(10n), TypeError)
|
assert.throws(() => pm.floor(10n), TypeError)
|
||||||
|
assert.strictEqual(pm.chain(5).add(7).value, 12)
|
||||||
pm.install(complexAdd)
|
pm.install(complexAdd)
|
||||||
pm.install(complexNegate)
|
pm.install(complexNegate)
|
||||||
pm.install(complexComplex)
|
pm.install(complexComplex)
|
||||||
@ -52,6 +55,9 @@ describe('A custom instance', () => {
|
|||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pm.floor(math.complex(1.9, 0)),
|
pm.floor(math.complex(1.9, 0)),
|
||||||
math.complex(1))
|
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", () => {
|
it("can defer definition of (even used) types", () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user