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 {Types} from './Types/bigint.mjs'
|
||||||
|
|
||||||
export const add = {
|
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 {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 {Types} from './Types/Complex.mjs'
|
||||||
|
|
||||||
export const add = {
|
export const add = {
|
||||||
'...Complex': {
|
'...Complex': ({self}) => addends => {
|
||||||
uses: ['self'],
|
if (addends.length === 0) return {re:0, im:0}
|
||||||
does: ref => addends => {
|
const seed = addends.shift()
|
||||||
if (addends.length === 0) return {re:0, im:0}
|
return addends.reduce(
|
||||||
const seed = addends.shift()
|
(w,z) => ({re: self(w.re, z.re), im: self(w.im, z.im)}), seed)
|
||||||
return addends.reduce((w,z) =>
|
|
||||||
({re: ref.self(w.re, z.re), im: ref.self(w.im, z.im)}), seed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ export const complex = {
|
|||||||
* have a numeric/scalar type, e.g. by implementing subtypes in
|
* have a numeric/scalar type, e.g. by implementing subtypes in
|
||||||
* typed-function
|
* 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 */
|
/* 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 {Types} from './Types/Complex.mjs'
|
||||||
|
|
||||||
export const negate = {
|
export const negate = {
|
||||||
Complex: {
|
Complex: ({self}) => z => ({re: self(z.re), im: self(z.im)})
|
||||||
uses: ['self'],
|
|
||||||
does: ref => z => {
|
|
||||||
return {re: ref.self(z.re), im: ref.self(z.im)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
/* 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'
|
||||||
export function use(dependencies, implementation) {
|
|
||||||
return [dependencies, implementation]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PocomathInstance {
|
export default class PocomathInstance {
|
||||||
/* Disallowed names for ops; beware, this is slightly non-DRY
|
/* 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:
|
* (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 only parameter ops gives the semantics of the operations to install.
|
||||||
* The keys are operation names. The value for a key is an object
|
* The keys are operation names. The value for a key is an object
|
||||||
* mapping (typed-function) signature strings to specifications of
|
* mapping each desired (typed-function) signature to a function taking
|
||||||
* of dependency lists and implementation functions.
|
* a dependency object to an implementation.
|
||||||
*
|
*
|
||||||
* A dependency list is a list of strings. Each string can either be the
|
* For more detail, such functions should have the format
|
||||||
* name of a function that the corresponding implementation has to call,
|
* ```
|
||||||
* or a specification of a particular signature of a function that it has
|
* ({depA, depB, depC: aliasC, ...}) => (opArg1, opArg2) => <result>
|
||||||
* to call, in the form 'FN(SIGNATURE)' [not implemented yet].
|
* ```
|
||||||
* Note the function name can be the special value 'self' to indicate a
|
* where the `depA`, `depB` etc. are the names of the
|
||||||
* recursive call to the given operation (either with or without a
|
* operations this implementation depends on; those operations can
|
||||||
* particular signature.
|
* 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
|
* You can specify that an operation depends on itself by using the
|
||||||
* list is empty, it should be a function taking the arguments specified
|
* special dependency identifier 'self'.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* There are various specifications currently allowed for the
|
* You can specify that an implementation depends on just a specific
|
||||||
* dependency list and implementation function:
|
* signature of the given operation by suffixing the dependency name
|
||||||
*
|
* with the signature in parentheses, e.g. `add(number,number)` to
|
||||||
* 1) Just a function. Then the dependency list is assumed to be empty.
|
* 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
|
||||||
* 2) A pair (= Array with two entries) of a dependency list and the
|
* operation in the body of the implementation. [NOTE: this signature-
|
||||||
* implementation function.
|
* specific reference is not yet implemented.]
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* Note that the "operation" named `Types` is special: it gives
|
* Note that the "operation" named `Types` is special: it gives
|
||||||
* types that must be installed in the instance. In this case, the keys
|
* 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 */
|
/* Grab all of the known deps */
|
||||||
for (const func in this._imps) {
|
for (const func in this._imps) {
|
||||||
if (func === 'Types') continue
|
if (func === 'Types') continue
|
||||||
for (const definition of Object.values(this._imps[func])) {
|
for (const {uses} of Object.values(this._imps[func])) {
|
||||||
let deps = []
|
for (const dependency of uses) {
|
||||||
if (Array.isArray(definition)) deps = definition[0]
|
|
||||||
else if (typeof definition === 'object') {
|
|
||||||
deps = definition.uses || deps
|
|
||||||
}
|
|
||||||
for (const dependency of deps) {
|
|
||||||
const depName = dependency.split('(',1)[0]
|
const depName = dependency.split('(',1)[0]
|
||||||
if (doneSet.has(depName)) continue
|
if (doneSet.has(depName)) continue
|
||||||
requiredSet.add(depName)
|
requiredSet.add(depName)
|
||||||
@ -137,14 +114,32 @@ export default class PocomathInstance {
|
|||||||
// new implementations, so set the op up to lazily recreate itself
|
// new implementations, so set the op up to lazily recreate itself
|
||||||
this._invalidate(name)
|
this._invalidate(name)
|
||||||
const opImps = this._imps[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 (signature in opImps) {
|
||||||
if (implementations[signature] === opImps[signature]) continue
|
if (does !== opImps[signature].does) {
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
`Conflicting definitions of ${signature} for ${name}`)
|
`Conflicting definitions of ${signature} for ${name}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
opImps[signature] = implementations[signature]
|
if (name === 'Types') {
|
||||||
for (const dep of implementations[signature][0] || []) {
|
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]
|
const depname = dep.split('(', 1)[0]
|
||||||
if (depname === 'self') continue
|
if (depname === 'self') continue
|
||||||
if (!(depname in this._affects)) {
|
if (!(depname in this._affects)) {
|
||||||
@ -187,27 +182,13 @@ export default class PocomathInstance {
|
|||||||
}
|
}
|
||||||
this._ensureTypes()
|
this._ensureTypes()
|
||||||
const tf_imps = {}
|
const tf_imps = {}
|
||||||
for (const signature in imps) {
|
for (const [signature, {uses, does}] of Object.entries(imps)) {
|
||||||
const specifier = imps[signature]
|
if (uses.length === 0) {
|
||||||
let deps = []
|
tf_imps[signature] = does()
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
const refs = {}
|
const refs = {}
|
||||||
let self_referential = false
|
let self_referential = false
|
||||||
for (const dep of deps) {
|
for (const dep of uses) {
|
||||||
// TODO: handle signature-specific dependencies
|
// TODO: handle signature-specific dependencies
|
||||||
if (dep.includes('(')) {
|
if (dep.includes('(')) {
|
||||||
throw new Error('signature specific reference unimplemented')
|
throw new Error('signature specific reference unimplemented')
|
||||||
@ -221,10 +202,10 @@ export default class PocomathInstance {
|
|||||||
if (self_referential) {
|
if (self_referential) {
|
||||||
tf_imps[signature] = this._typed.referToSelf(self => {
|
tf_imps[signature] = this._typed.referToSelf(self => {
|
||||||
refs.self = self
|
refs.self = self
|
||||||
return imp(refs)
|
return does(refs)
|
||||||
})
|
})
|
||||||
} else {
|
} 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 = {
|
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 {Types} from './Types/number.mjs'
|
||||||
|
|
||||||
export const add = {
|
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 { 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', () => {
|
it('can be extended', () => {
|
||||||
math.install({'add': {
|
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')
|
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')
|
const pi = new PocomathInstance('dummy')
|
||||||
describe('PocomathInstance', () => {
|
describe('PocomathInstance', () => {
|
||||||
it('creates an instance that can define typed-functions', () => {
|
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(2,2), 4)
|
||||||
assert.strictEqual(pi.add('Kilroy', 17), 'Kilroy17')
|
assert.strictEqual(pi.add('Kilroy', 17), 'Kilroy17')
|
||||||
assert.strictEqual(pi.add(1, undefined), NaN)
|
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