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 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 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 const add = {
'...Complex': {
uses: ['self'],
does: ref => 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)
}
'...Complex': ({self}) => addends => {
if (addends.length === 0) return {re:0, im:0}
const seed = addends.shift()
return addends.reduce(
(w,z) => ({re: self(w.re, z.re), im: 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
* 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
}

View File

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

View File

@ -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 (does != opImps[signature]) {
throw newSyntaxError(
`Conflicting definitions of type ${signature}`)
}
} else {
opImps[signature] = does
}
continue
}
if (signature in opImps) {
if (implementations[signature] === opImps[signature]) continue
throw new SyntaxError(
`Conflicting definitions of ${signature} for ${name}`)
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)
}
}
}

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 = {
'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 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 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', () => {
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')
})

View File

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

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