feat: Add and illustrate multiple ways of specifying implementations (#19)

Resolves #9.

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: #19
This commit is contained in:
Glen Whitney 2022-07-23 05:06:48 +00:00
parent 4fdafc751e
commit d72c443616
9 changed files with 81 additions and 22 deletions

View File

@ -16,6 +16,10 @@ Furthermore, note that 'Complex' is implemented in a way that doesn't care about
This core could be extended with many more operations, and more types could be defined, and additional sub-bundles like `number/all.mjs` or clever conditional loaders like `complex/extendToComplex.mjs` could be defined. This core could be extended with many more operations, and more types could be defined, and additional sub-bundles like `number/all.mjs` or clever conditional loaders like `complex/extendToComplex.mjs` could be defined.
Also see the comments for the public member functions of
`core/PocomathInstance.mjs` for further details on the structure and API of this
scheme for organizing a CAS.
Hopefully this shows promise. It is an evolution of the concept first prototyped in [picomath](https://code.studioinfinity.org/glen/picomath). However, picomath depended on typed-function allowing mutable function entities, which turned out not to be performant. Pocomath, on the other hand, uses typed-function v3 as it stands, although it does suggest that it would be helpful to extend typed-function with subtypes, and it could even be reasonable to move the dependency tracking into typed-function itself (given that typed-function already supports self-dependencies, it would not be difficult to extend that to inter-dependencies between different typed-functions). Hopefully this shows promise. It is an evolution of the concept first prototyped in [picomath](https://code.studioinfinity.org/glen/picomath). However, picomath depended on typed-function allowing mutable function entities, which turned out not to be performant. Pocomath, on the other hand, uses typed-function v3 as it stands, although it does suggest that it would be helpful to extend typed-function with subtypes, and it could even be reasonable to move the dependency tracking into typed-function itself (given that typed-function already supports self-dependencies, it would not be difficult to extend that to inter-dependencies between different typed-functions).
Note the conception of Pocomath includes allowing one implementation to depend just on a specific signature of another function, for efficiency's sake (if for example 'bar(Matrix)' knows it will only call 'foo(Matrix)', it avoids another type-dispatch). That capability did not actually come up in this toy example, so it remains unimplemented, but it should and could easily be added. Note the conception of Pocomath includes allowing one implementation to depend just on a specific signature of another function, for efficiency's sake (if for example 'bar(Matrix)' knows it will only call 'foo(Matrix)', it avoids another type-dispatch). That capability did not actually come up in this toy example, so it remains unimplemented, but it should and could easily be added.

View File

@ -1,5 +1,6 @@
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': [[], addends => addends.reduce((x,y) => x+y, 0n)], '...bigint': use([], addends => addends.reduce((x,y) => x+y, 0n))
} }

View File

@ -1,3 +1,4 @@
import {use} from '../core/PocomathInstance.mjs'
export {Types} from './Types/bigint.mjs' export {Types} from './Types/bigint.mjs'
export const negate = {bigint: [[], b => -b ]} export const negate = {bigint: use([], b => -b)}

View File

@ -1,10 +1,13 @@
export {Types} from './Types/Complex.mjs' export {Types} from './Types/Complex.mjs'
export const add = { export const add = {
'...Complex': [['self'], ref => addends => { '...Complex': {
if (addends.length === 0) return {re:0, im:0} uses: ['self'],
const seed = addends.shift() does: ref => addends => {
return addends.reduce((w,z) => if (addends.length === 0) return {re:0, im:0}
({re: ref.self(w.re, z.re), im: ref.self(w.im, z.im)}), seed) const seed = addends.shift()
}] 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': [[], (x, y) => ({re: x, im: y})], 'any, any': {does: (x, y) => ({re: x, im: y})},
/* Take advantage of conversions in typed-function */ /* Take advantage of conversions in typed-function */
Complex: [[], z => z] Complex: {does: z => z}
} }

View File

@ -1,7 +1,10 @@
export {Types} from './Types/Complex.mjs' export {Types} from './Types/Complex.mjs'
export const negate = { export const negate = {
Complex: [['self'], ref => z => { Complex: {
return {re: ref.self(z.re), im: ref.self(z.im)} uses: ['self'],
}] does: ref => z => {
return {re: ref.self(z.re), im: ref.self(z.im)}
}
}
} }

View File

@ -1,6 +1,10 @@
/* Core of pocomath: create an instance */ /* Core of pocomath: create an instance */
import typed from 'typed-function' import typed from 'typed-function'
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
* in that if a new top-level PocomathInstance method is added, its name * in that if a new top-level PocomathInstance method is added, its name
@ -24,15 +28,16 @@ export default class PocomathInstance {
* @param {Object<string, Object<Signature, [string[], function]>>} ops * @param {Object<string, Object<Signature, [string[], function]>>} 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 pairs of dependency * mapping (typed-function) signature strings to specifications of
* lists and implementation functions. * of dependency lists and implementation functions.
* *
* A dependency list is a list of strings. Each string can either be the * 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, * name of a function that the corresponding implementation has to call,
* or a specification of a particular signature of a function that it has * or a specification of a particular signature of a function that it has
* to call, in the form 'FN(SIGNATURE)'. Note the function name can be * to call, in the form 'FN(SIGNATURE)' [not implemented yet].
* the special value 'self' to indicate a recursive call to the given * Note the function name can be the special value 'self' to indicate a
* operation (either with or without a particular signature. * recursive call to the given operation (either with or without a
* particular signature.
* *
* There are two cases for the implementation function. If the dependency * There are two cases for the implementation function. If the dependency
* list is empty, it should be a function taking the arguments specified * list is empty, it should be a function taking the arguments specified
@ -41,6 +46,30 @@ export default class PocomathInstance {
* requested functions as values, to a function taking the arguments * requested functions as values, to a function taking the arguments
* specified by the signature and returning the value. * specified by the signature and returning the value.
* *
* 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.
*
* 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
* are type names, and the values are objects with a property 'test' * are type names, and the values are objects with a property 'test'
@ -66,7 +95,12 @@ export default class PocomathInstance {
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 definition of Object.values(this._imps[func])) {
for (const dependency of definition[0]) { let deps = []
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)
@ -154,7 +188,20 @@ export default class PocomathInstance {
this._ensureTypes() this._ensureTypes()
const tf_imps = {} const tf_imps = {}
for (const signature in imps) { for (const signature in imps) {
const [deps, imp] = imps[signature] 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) { if (deps.length === 0) {
tf_imps[signature] = imp tf_imps[signature] = imp
} else { } else {

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}