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.
This commit is contained in:
parent
d72c443616
commit
9fb3aa2959
@ -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)
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -1,13 +1,10 @@
|
||||
export {Types} from './Types/Complex.mjs'
|
||||
|
||||
export const add = {
|
||||
'...Complex': {
|
||||
uses: ['self'],
|
||||
does: ref => addends => {
|
||||
'...Complex': ({self}) => 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)
|
||||
}
|
||||
return addends.reduce(
|
||||
(w,z) => ({re: self(w.re, z.re), im: self(w.im, z.im)}), seed)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)})
|
||||
}
|
||||
|
@ -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<string, Object<Signature, [string[], function]>>} ops
|
||||
* @param {Object<string, Object<Signature, ({deps})=> 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) => <result>
|
||||
* ```
|
||||
* 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 '<result>`, 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 (implementations[signature] === opImps[signature]) continue
|
||||
if (does != opImps[signature]) {
|
||||
throw newSyntaxError(
|
||||
`Conflicting definitions of type ${signature}`)
|
||||
}
|
||||
} else {
|
||||
opImps[signature] = does
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (signature in opImps) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
src/core/dependencyExtractor.mjs
Normal file
9
src/core/dependencyExtractor.mjs
Normal file
@ -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) }
|
||||
})
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
export { Types } from './Types/number.mjs'
|
||||
|
||||
export const negate = {number: n => -n}
|
||||
export const negate = {number: () => n => -n}
|
||||
|
@ -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')
|
||||
})
|
||||
|
@ -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)
|
||||
|
22
test/core/_dependencyExtractor.mjs
Normal file
22
test/core/_dependencyExtractor.mjs
Normal file
@ -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)
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in New Issue
Block a user