fix: Separate typed instance for each PocomathInstance (#15)

Also starts each PocomathInstance with no types at all, and uses the new
  situation to eliminate the need for a Complex "base case".

  Resolves #14.
  Resolves #13.

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: #15
This commit is contained in:
Glen Whitney 2022-07-22 20:49:14 +00:00
parent ed71b15969
commit 0069597a76
25 changed files with 120 additions and 74 deletions

View File

@ -1,3 +0,0 @@
import typed from 'typed-function'
typed.addType({name: 'bigint', test: b => typeof b === 'bigint'})

View File

@ -1,15 +0,0 @@
import {numComplex} from './types/Complex.mjs'
export const add = {
'...Complex': [['self'], ref => addends => {
if (addends.length === 0) return {re:0, im:0}
const seed = addends.shift()
return addends.reduce((w,z) => {
/* Need a "base case" to avoid infinite self-reference loops */
if (numComplex(z) && numComplex(w)) {
return {re: w.re + z.re, im: w.im + z.im}
}
return {re: ref.self(w.re, z.re), im: ref.self(w.im, z.im)}
}, seed)
}]
}

View File

@ -1,8 +0,0 @@
import {numComplex} from './types/Complex.mjs'
export const negate = {
Complex: [['self'], ref => z => {
/* need a "base case" to avoid infinite self-reference */
if (numComplex(z)) return {re: -z.re, im: -z.im}
return {re: ref.self(z.re), im: ref.self(z.im)}
}]
}

View File

@ -1,3 +0,0 @@
export const negate = {
number: [[], n => -n]
}

View File

@ -5,7 +5,9 @@
inclusion, avoiding factory functions.',
main: 'index.js',
scripts: {
test: '!(find . | sort -f | uniq -i -c | grep -v " 1 ") && npx mocha',
'test:filecase': '!(find . | sort -f | uniq -i -c | grep -v " 1 ")',
'test:unit': 'npx mocha --recursive',
test: 'pnpm test:filecase && pnpm test:unit',
},
repository: {
type: 'git',

View File

@ -0,0 +1,3 @@
export const Types = {
bigint: {test: b => typeof b === 'bigint'}
}

View File

@ -1,4 +1,5 @@
import './BigInt.mjs'
export {Types} from './Types/bigint.mjs'
export const add = {
'...bigint': [[], addends => addends.reduce((x,y) => x+y, 0n)],
}

View File

@ -1,3 +1,4 @@
export {Types} from './Types/bigint.mjs'
export {add} from './add.mjs'
export {negate} from './negate.mjs'
export {subtract} from '../generic/subtract.mjs'

View File

@ -1,2 +1,3 @@
import './BigInt.mjs'
export {Types} from './Types/bigint.mjs'
export const negate = {bigint: [[], b => -b ]}

View File

@ -1,5 +1,3 @@
import typed from 'typed-function'
/* Use a plain object with keys re and im for a complex; note the components
* can be any type (for this proof-of-concept; in reality we'd want to
* insist on some numeric or scalar supertype).
@ -8,20 +6,13 @@ export function isComplex(z) {
return z && typeof z === 'object' && 're' in z && 'im' in z
}
typed.addType({name: 'Complex', test: isComplex})
typed.addConversion({
from: 'number',
to: 'Complex',
convert: x => ({re: x, im: 0})
})
/* Pleasantly enough, it is OK to add this conversion even if there is no
* type 'bigint' defined, so everything should Just Work.
*/
typed.addConversion({
from: 'bigint',
to: 'Complex',
convert: x => ({re: x, im: 0n})
})
export const Types = {
Complex: {
test: isComplex,
number: x => ({re: x, im: 0}),
bigint: x => ({re: x, im: 0n})
}
}
/* test if an entity is Complex<number>, so to speak: */
export function numComplex(z) {

10
src/complex/add.mjs Normal file
View File

@ -0,0 +1,10 @@
export {Types} from './Types/Complex.mjs'
export const add = {
'...Complex': [['self'], 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)
}]
}

View File

@ -1,3 +1,4 @@
export {Types} from './Types/Complex.mjs'
export {complex} from './complex.mjs'
export {add} from './add.mjs'
export {negate} from './negate.mjs'

View File

@ -1,4 +1,4 @@
import './types/Complex.mjs'
export {Types} from './Types/Complex.mjs'
export const complex = {
/* Very permissive for sake of proof-of-concept; would be better to

View File

@ -1,4 +1,3 @@
import './types/Complex.mjs'
import * as complex from './complex.mjs'
/* Add all the complex implementations for functions already

7
src/complex/negate.mjs Normal file
View File

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

View File

@ -6,6 +6,10 @@ export default class PocomathInstance {
this.name = name
this._imps = {}
this._affects = {}
this._typed = typed.create()
this._typed.clear()
// Convenient hack for now, would remove when a real string type is added:
this._typed.addTypes([{name: 'string', test: s => typeof s === 'string'}])
}
/**
@ -29,7 +33,14 @@ export default class PocomathInstance {
* 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
* specified by the signature and returning the value.
*
* Note that the "operation" named `Types` is special: it gives
* 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'
* giving the predicate for the type, and properties for each type that can
* be converted **to** this type, giving the corresponding conversion
* function.
*/
install(ops) {
for (const key in ops) this._installOp(key, ops[key])
@ -47,7 +58,7 @@ export default class PocomathInstance {
`Conflicting definitions of ${signature} for ${name}`)
} else {
opImps[signature] = implementations[signature]
for (const dep of implementations[signature][0]) {
for (const dep of implementations[signature][0] || []) {
const depname = dep.split('(', 1)[0]
if (depname === 'self') continue
if (!(depname in this._affects)) {
@ -87,6 +98,7 @@ export default class PocomathInstance {
if (!imps || Object.keys(imps).length === 0) {
throw new SyntaxError(`No implementations for ${name}`)
}
this._ensureTypes()
const tf_imps = {}
for (const signature in imps) {
const [deps, imp] = imps[signature]
@ -107,7 +119,7 @@ export default class PocomathInstance {
}
}
if (self_referential) {
tf_imps[signature] = typed.referToSelf(self => {
tf_imps[signature] = this._typed.referToSelf(self => {
refs.self = self
return imp(refs)
})
@ -116,7 +128,7 @@ export default class PocomathInstance {
}
}
}
const tf = typed(name, tf_imps)
const tf = this._typed(name, tf_imps)
this[name] = tf
return tf
}
@ -127,7 +139,40 @@ export default class PocomathInstance {
*/
_ensureBundle(name) {
const maybe = this[name]
if (typed.isTypedFunction(maybe)) return maybe
if (this._typed.isTypedFunction(maybe)) return maybe
return this._bundle(name)
}
/**
* Ensure that all of the requested types and conversions are actually
* in the typed-function universe:
*/
_ensureTypes() {
const newTypes = []
const newTypeSet = new Set()
const knownTypeSet = new Set()
const conversions = []
const typeSpec = this._imps.Types
for (const name in this._imps.Types) {
knownTypeSet.add(name)
for (const from in typeSpec[name]) {
if (from === 'test') continue;
conversions.push(
{from, to: name, convert: typeSpec[name][from]})
}
try { // Hack: work around typed-function #154
this._typed._findType(name)
} catch {
newTypeSet.add(name)
newTypes.push({name, test: typeSpec[name].test})
}
}
this._typed.addTypes(newTypes)
const newConversions = conversions.filter(
item => (newTypeSet.has(item.from) || newTypeSet.has(item.to)) &&
knownTypeSet.has(item.from) && knownTypeSet.has(item.to)
)
this._typed.addConversions(newConversions)
}
}

View File

@ -0,0 +1,7 @@
export const Types = {
number: {
test: n => typeof n === 'number',
string: s => +s
}
}

View File

@ -1,3 +1,5 @@
export {Types} from './Types/number.mjs'
export const add = {
'...number': [[], addends => addends.reduce((x,y) => x+y, 0)],
}

View File

@ -1,3 +1,4 @@
export {Types} from './Types/number.mjs'
export {add} from './add.mjs'
export {negate} from './negate.mjs'
export {subtract} from '../generic/subtract.mjs'

3
src/number/negate.mjs Normal file
View File

@ -0,0 +1,3 @@
export { Types } from './Types/number.mjs'
export const negate = {number: [[], n => -n]}

View File

@ -1,5 +1,5 @@
/* Core of pocomath: generates the default instance */
import PocomathInstance from './PocomathInstance.mjs'
import PocomathInstance from './core/PocomathInstance.mjs'
import * as numbers from './number/all.mjs'
import * as bigints from './bigint/all.mjs'
import * as complex from './complex/all.mjs'

View File

@ -1,5 +1,5 @@
import assert from 'assert'
import math from '../pocomath.mjs'
import math from '../src/pocomath.mjs'
describe('The default full pocomath instance "math"', () => {
it('can subtract numbers', () => {

View File

@ -1,5 +1,5 @@
import assert from 'assert'
import PocomathInstance from '../PocomathInstance.mjs'
import PocomathInstance from '../../src/core/PocomathInstance.mjs'
describe('PocomathInstance', () => {
it('creates an instance that can define typed-functions', () => {

View File

@ -1,26 +1,26 @@
import assert from 'assert'
import math from '../pocomath.mjs'
import typed from 'typed-function'
import PocomathInstance from '../PocomathInstance.mjs'
import * as numbers from '../number/all.mjs'
import * as numberAdd from '../number/add.mjs'
import * as complex from '../complex/all.mjs'
import * as complexAdd from '../complex/add.mjs'
import * as complexNegate from '../complex/negate.mjs'
import extendToComplex from '../complex/extendToComplex.mjs'
import math from '../src/pocomath.mjs'
import PocomathInstance from '../src/core/PocomathInstance.mjs'
import * as numbers from '../src/number/all.mjs'
import * as numberAdd from '../src/number/add.mjs'
import * as complex from '../src/complex/all.mjs'
import * as complexAdd from '../src/complex/add.mjs'
import * as complexNegate from '../src/complex/negate.mjs'
import extendToComplex from '../src/complex/extendToComplex.mjs'
const bw = new PocomathInstance('backwards')
describe('A custom instance', () => {
it("works when partially assembled", () => {
bw.install(complex)
assert.deepStrictEqual(bw.add(2, bw.complex(0, 3)), {re: 2, im: 3})
assert.deepStrictEqual(bw.negate(2), bw.complex(-2,-0))
assert.deepStrictEqual(bw.subtract(2, bw.complex(0, 3)), {re: 2, im: -3})
// Not much we can call without any number types:
assert.deepStrictEqual(bw.complex(0, 3), {re: 0, im: 3})
// Don't have a way to negate things, for example:
assert.throws(() => bw.negate(2), TypeError)
})
it("can be assembled in any order", () => {
bw.install(numbers)
typed.addConversion({from: 'string', to: 'number', convert: x => +x})
bw.install({Types: {string: {test: s => typeof s === 'string'}}})
assert.strictEqual(bw.subtract(16, bw.add(3,4,2)), 7)
assert.strictEqual(bw.negate('8'), -8)
assert.deepStrictEqual(bw.add(bw.complex(1,3), 1), {re: 2, im: 3})
@ -36,7 +36,7 @@ describe('A custom instance', () => {
assert.strictEqual(pm.subtract(5, 10), -5)
pm.install(complexAdd)
pm.install(complexNegate)
// Should be enough to allow complex subtraction, as subtract is generic
// Should be enough to allow complex subtraction, as subtract is generic:
assert.deepStrictEqual(
pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1})
})
@ -45,8 +45,9 @@ describe('A custom instance', () => {
const cherry = new PocomathInstance('cherry')
cherry.install(numberAdd)
await extendToComplex(cherry)
// Now we have an instance that supports addition for number and complex
// and little else:
/* Now we have an instance that supports addition for number and complex
and little else:
*/
assert.strictEqual(cherry.add(3, 4, 2), 9)
assert.deepStrictEqual(
cherry.add(cherry.complex(3, 3), 4, cherry.complex(2, 2)),