feat: Implement signature-specifc reference

Also implements a config object that upon change, lazily invalidates
  all operations that access it.

  Also allows references to signatures with nonexistent types (which
  typed-function does not); they come back as undefined.

  Uses these features to implement sqrt for number and complex.

  Resolves #7.
This commit is contained in:
Glen Whitney 2022-07-25 04:20:13 -07:00
parent 79f261ff65
commit 91ec20edd8
21 changed files with 256 additions and 26 deletions

5
src/complex/abs.mjs Normal file
View File

@ -0,0 +1,5 @@
export {Types} from './Types/Complex.mjs'
export const abs = {Complex: ({sqrt, add, multiply}) => z => {
return sqrt(add(multiply(z.re, z.re), multiply(z.im, z.im)))
}}

View File

@ -1,5 +1,2 @@
export {Types} from './Types/Complex.mjs' export * from './native.mjs'
export {complex} from './complex.mjs' export * from '../generic/arithmetic.mjs'
export {add} from './add.mjs'
export {negate} from './negate.mjs'
export {subtract} from '../generic/subtract.mjs'

8
src/complex/native.mjs Normal file
View File

@ -0,0 +1,8 @@
export {Types} from './Types/Complex.mjs'
export {abs} from './abs.mjs'
export {add} from './add.mjs'
export {complex} from './complex.mjs'
export {negate} from './negate.mjs'
export {sqrt} from './sqrt.mjs'

37
src/complex/sqrt.mjs Normal file
View File

@ -0,0 +1,37 @@
export { Types } from './Types/Complex.mjs'
export const sqrt = {
Complex: ({
config,
complex,
multiply,
sign,
self,
divide,
add,
'abs(Complex)': abs,
subtract
}) => {
if (config.predictable) {
return z => {
const imSign = sign(z.im)
const reSign = sign(z.re)
if (imSign === 0 && reSign === 1) return complex(self(z.re))
return complex(
multiply(sign(z.im), self(divide(add(abs(z),z.re), 2))),
self(divide(subtract(abs(z),z.re), 2))
)
}
}
return z => {
const imSign = sign(z.im)
const reSign = sign(z.re)
if (imSign === 0 && reSign === 1) return self(z.re)
return complex(
multiply(sign(z.im), self(divide(add(abs(z),z.re), 2))),
self(divide(subtract(abs(z),z.re), 2))
)
}
}
}

View File

@ -8,7 +8,7 @@ export default class PocomathInstance {
* 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
* must be added to this list. * must be added to this list.
*/ */
static reserved = new Set(['install', 'importDependencies']) static reserved = new Set(['config', 'importDependencies', 'install', 'name'])
constructor(name) { constructor(name) {
this.name = name this.name = name
@ -17,6 +17,19 @@ export default class PocomathInstance {
this._typed = typed.create() this._typed = typed.create()
this._typed.clear() this._typed.clear()
this.Types = {any: {}} // dummy entry to track the default 'any' type this.Types = {any: {}} // dummy entry to track the default 'any' type
this._doomed = new Set() // for detecting circular reference
this._config = {predictable: false}
const self = this
this.config = new Proxy(this._config, {
get: (target, property) => target[property],
set: (target, property, value) => {
if (value !== target[property]) {
target[property] = value
self._invalidateDependents('config')
}
return true // successful
}
})
} }
/** /**
@ -80,6 +93,8 @@ export default class PocomathInstance {
* @param {string[]} types A list of type names * @param {string[]} types A list of type names
*/ */
async importDependencies(types) { async importDependencies(types) {
const typeSet = new Set(types)
typeSet.add('generic')
const doneSet = new Set(['self']) // nothing to do for self dependencies const doneSet = new Set(['self']) // nothing to do for self dependencies
while (true) { while (true) {
const requiredSet = new Set() const requiredSet = new Set()
@ -96,7 +111,7 @@ export default class PocomathInstance {
} }
if (requiredSet.size === 0) break if (requiredSet.size === 0) break
for (const name of requiredSet) { for (const name of requiredSet) {
for (const type of types) { for (const type of typeSet) {
try { try {
const modName = `../${type}/${name}.mjs` const modName = `../${type}/${name}.mjs`
const mod = await import(modName) const mod = await import(modName)
@ -145,7 +160,8 @@ export default class PocomathInstance {
} }
} }
this.Types[type] = spec this.Types[type] = spec
this._invalidate(':' + type) // rebundle anything that uses the new type // rebundle anything that uses the new type:
this._invalidateDependents(':' + type)
} }
} }
@ -198,14 +214,27 @@ export default class PocomathInstance {
* and if it has no implementations so far, set them up. * and if it has no implementations so far, set them up.
*/ */
_invalidate(name) { _invalidate(name) {
if (this._doomed.has(name)) {
/* In the midst of a circular invalidation, so do nothing */
return
}
if (!(name in this._imps)) {
this._imps[name] = {}
}
this._doomed.add(name)
this._invalidateDependents(name)
this._doomed.delete(name)
const self = this const self = this
Object.defineProperty(this, name, { Object.defineProperty(this, name, {
configurable: true, configurable: true,
get: () => self._bundle(name) get: () => self._bundle(name)
}) })
if (!(name in this._imps)) { }
this._imps[name] = {}
} /**
* Invalidate all the dependents of a given property of the instance
*/
_invalidateDependents(name) {
if (name in this._affects) { if (name in this._affects) {
for (const ancestor of this._affects[name]) { for (const ancestor of this._affects[name]) {
this._invalidate(ancestor) this._invalidate(ancestor)
@ -229,29 +258,69 @@ export default class PocomathInstance {
`Every implementation for ${name} uses an undefined type;\n` `Every implementation for ${name} uses an undefined type;\n`
+ ` signatures: ${Object.keys(imps)}`) + ` signatures: ${Object.keys(imps)}`)
} }
Object.defineProperty(this, name, {configurable: true, value: 'limbo'})
const tf_imps = {} const tf_imps = {}
for (const [signature, {uses, does}] of usableEntries) { for (const [signature, {uses, does}] of usableEntries) {
if (uses.length === 0) { if (uses.length === 0) {
tf_imps[signature] = does() tf_imps[signature] = does()
} else { } else {
const refs = {} const refs = {}
let self_referential = false let full_self_referential = false
let part_self_references = []
for (const dep of uses) { for (const dep of uses) {
// TODO: handle signature-specific dependencies const [func, needsig] = dep.split(/[()]/)
if (dep.includes('(')) { if (func === 'self') {
throw new Error('signature specific reference unimplemented') if (needsig) {
} if (full_self_referential) {
if (dep === 'self') { throw new SyntaxError(
self_referential = true 'typed-function does not support mixed full and '
+ 'partial self-reference')
}
if (subsetOfKeys(typesOfSignature(needsig), this.Types)) {
part_self_references.push(needsig)
}
} else {
if (part_self_references.length) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
full_self_referential = true
}
} else { } else {
refs[dep] = this[dep] // assume acyclic for now if (this[func] === 'limbo') {
/* We are in the midst of bundling func, so have to use
* an indirect reference to func. And given that, there's
* really no helpful way to extract a specific signature
*/
const self = this
refs[dep] = function () { // is this the most efficient?
return self[func].apply(this, arguments)
}
} else {
// can bundle up func, and grab its signature if need be
let destination = this[func]
if (needsig) {
destination = this._typed.find(destination, needsig)
}
refs[dep] = destination
}
} }
} }
if (self_referential) { if (full_self_referential) {
tf_imps[signature] = this._typed.referToSelf(self => { tf_imps[signature] = this._typed.referToSelf(self => {
refs.self = self refs.self = self
return does(refs) return does(refs)
}) })
} else if (part_self_references.length) {
tf_imps[signature] = this._typed.referTo(
...part_self_references, (...impls) => {
for (let i = 0; i < part_self_references.length; ++i) {
refs[`self(${part_self_references[i]})`] = impls[i]
}
return does(refs)
}
)
} else { } else {
tf_imps[signature] = does(refs) tf_imps[signature] = does(refs)
} }

View File

@ -4,6 +4,9 @@
*/ */
export default function dependencyExtractor(destinationSet) { export default function dependencyExtractor(destinationSet) {
return new Proxy({}, { return new Proxy({}, {
get: (target, property) => { destinationSet.add(property) } get: (target, property) => {
destinationSet.add(property)
return {}
}
}) })
} }

1
src/generic/all.mjs Normal file
View File

@ -0,0 +1 @@
export * from './arithmetic.mjs'

View File

@ -0,0 +1,3 @@
export {divide} from './divide.mjs'
export {sign} from './sign.mjs'
export {subtract} from './subtract.mjs'

4
src/generic/divide.mjs Normal file
View File

@ -0,0 +1,4 @@
export const divide = {
'any,any': ({multiply, invert}) => (x, y) => multiply(x, invert(y))
}

6
src/generic/sign.mjs Normal file
View File

@ -0,0 +1,6 @@
export const sign = {
any: ({negate, divide, abs}) => x => {
if (x === negate(x)) return x // zero
return divide(x, abs(x))
}
}

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

@ -0,0 +1,3 @@
export {Types} from './Types/number.mjs'
export const abs = {number: () => n => Math.abs(n)}

View File

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

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

@ -0,0 +1,3 @@
export {Types} from './Types/number.mjs'
export const invert = {number: () => n => 1/n}

5
src/number/multiply.mjs Normal file
View File

@ -0,0 +1,5 @@
export {Types} from './Types/number.mjs'
export const multiply = {
'...number': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1),
}

4
src/number/native.js Normal file
View File

@ -0,0 +1,4 @@
export {Types} from './Types/number.mjs'
export {add} from './add.mjs'
export {negate} from './negate.mjs'
export {sqrt} from './sqrt.mjs'

8
src/number/native.mjs Normal file
View File

@ -0,0 +1,8 @@
export {Types} from './Types/number.mjs'
export {abs} from './abs.mjs'
export {add} from './add.mjs'
export {invert} from './invert.mjs'
export {multiply} from './multiply.mjs'
export {negate} from './negate.mjs'
export {sqrt} from './sqrt.mjs'

14
src/number/sqrt.mjs Normal file
View File

@ -0,0 +1,14 @@
export { Types } from './Types/number.mjs'
export const sqrt = {
number: ({config, complex, 'self(Complex)': complexSqrt}) => {
if (config.predictable || !complexSqrt) {
return n => isNaN(n) ? NaN : Math.sqrt(n)
}
return n => {
if (isNaN(n)) return NaN
if (n >= 0) return Math.sqrt(n)
return complexSqrt(complex(n))
}
}
}

View File

@ -1,12 +1,14 @@
/* Core of pocomath: generates the default instance */ /* Core of pocomath: generates the default instance */
import PocomathInstance from './core/PocomathInstance.mjs' import PocomathInstance from './core/PocomathInstance.mjs'
import * as numbers from './number/all.mjs' import * as numbers from './number/native.mjs'
import * as bigints from './bigint/all.mjs' import * as bigints from './bigint/all.mjs'
import * as complex from './complex/all.mjs' import * as complex from './complex/native.mjs'
import * as generic from './generic/all.mjs'
const math = new PocomathInstance('math') const math = new PocomathInstance('math')
math.install(numbers) math.install(numbers)
math.install(bigints) math.install(bigints)
math.install(complex) math.install(complex)
math.install(generic)
export default math export default math

0
test/complex/## Executable file
View File

29
test/complex/_all.mjs Normal file
View File

@ -0,0 +1,29 @@
import assert from 'assert'
import math from '../../src/pocomath.mjs'
import PocomathInstance from '../../src/core/PocomathInstance.mjs'
import * as complexSqrt from '../../src/complex/sqrt.mjs'
describe('complex', () => {
it('supports sqrt', () => {
assert.deepStrictEqual(math.sqrt(math.complex(1,0)), 1)
assert.deepStrictEqual(
math.sqrt(math.complex(0,1)),
math.complex(math.sqrt(0.5), math.sqrt(0.5)))
math.config.predictable = true
assert.deepStrictEqual(math.sqrt(math.complex(1,0)), math.complex(1,0))
assert.deepStrictEqual(
math.sqrt(math.complex(0,1)),
math.complex(math.sqrt(0.5), math.sqrt(0.5)))
math.config.predictable = false
})
it('can bundle sqrt', async function () {
const ms = new PocomathInstance('Minimal Sqrt')
ms.install(complexSqrt)
await ms.importDependencies(['number', 'complex'])
assert.deepStrictEqual(
ms.sqrt(math.complex(0, -1)),
math.complex(ms.negate(ms.sqrt(0.5)), ms.sqrt(0.5)))
})
})

29
test/number/_all.mjs Normal file
View File

@ -0,0 +1,29 @@
import assert from 'assert'
import math from '../../src/pocomath.mjs'
import PocomathInstance from '../../src/core/PocomathInstance.mjs'
import * as numberSqrt from '../../src/number/sqrt.mjs'
import * as complex from '../../src/complex/all.mjs'
import * as numbers from '../../src/number/all.mjs'
describe('number', () => {
it('supports sqrt', () => {
assert.strictEqual(math.sqrt(4), 2)
assert.strictEqual(math.sqrt(NaN), NaN)
assert.strictEqual(math.sqrt(2.25), 1.5)
assert.deepStrictEqual(math.sqrt(-9), math.complex(0, 3))
math.config.predictable = true
assert.strictEqual(math.sqrt(-9), NaN)
math.config.predictable = false
assert.deepStrictEqual(math.sqrt(-0.25), math.complex(0, 0.5))
})
it('supports sqrt by itself', () => {
const no = new PocomathInstance('Numbers Only')
no.install(numberSqrt)
assert.strictEqual(no.sqrt(2.56), 1.6)
assert.strictEqual(no.sqrt(-17), NaN)
no.install(complex)
no.install(numbers)
assert.deepStrictEqual(no.sqrt(-16), no.complex(0,4))
})
})