Merge pull request 'feat: Switch to function-based specification of dependencies' () from ultimate_notation into main

Reviewed-on: 
This commit is contained in:
Glen Whitney 2022-07-23 16:59:21 +00:00
commit 890752a1e7
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 => {
'...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)
}
}

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

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