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:
Glen Whitney 2022-07-23 09:55:02 -07:00
parent d72c443616
commit 9fb3aa2959
13 changed files with 104 additions and 102 deletions

View File

@ -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)
} }

View File

@ -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}

View File

@ -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)
}
} }
} }

View File

@ -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
} }

View File

@ -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)}
}
}
} }

View File

@ -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)
} }
} }
} }

View 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) }
})
}

View File

@ -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))
} }

View File

@ -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),
} }

View File

@ -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}

View File

@ -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')
}) })

View File

@ -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)

View 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)
})
})