feat: Add all remaining features of original Picomath PoC (#5)

Namely, a README describing the proof-of-concept,
  a custom selective loader, and some additional tests.

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: #5
This commit is contained in:
Glen Whitney 2022-07-19 19:37:52 +00:00
parent 84a8b9d5c4
commit a16d6a5ce3
4 changed files with 67 additions and 1 deletions

View File

@ -1,3 +1,21 @@
# pocomath
A little proof-of-concept for organizing mathjs by module inclusion, avoiding factory functions.
A little proof-of-concept for organizing mathjs by module inclusion, avoiding factory functions.
Note this project is package-managed by [pnpm](https://pnpm.io/). I do not expect that a clone can easily be manipulated with `npm`.
Defines a class PocomathInstance to embody independent instances of a mathjs-style CAS. Basically, it keeps track of a collection of implementations (in the sense of typed-function) for each of the functions to be used in the CAS, rather than just the finalized typed-functions. It also tracks the dependencies of each implementation (which must form a directed acyclic network). When a method is requested from the instance, it assembles the proper typed-function (and caches it, of course). Whenever an implementation is added to that function name or any of its dependencies, the previously assembled typed-function is discarded, so that a new one will be constructed on its next use.
Multiple different instances can coexist and have different collections of operations. Moreover, only the source files for the operations actually desired are ever visited in the import tree, so minimizing a bundle for a specific subset of operations should be quite straightforward.
Hopefully the test cases, especially `test/_pocomath.mjs` and `test/custom.js`, will show off these aspects in action.
Note that 'subtract' is implemented as a 'generic' operation, that depends only on the 'add' and 'negate' operations (and so doesn't care what types it is operating on).
Furthermore, note that 'Complex' is implemented in a way that doesn't care about the types of the real and imaginary components, so with the 'bigint' type defined here as well, we obtain Gaussian integers for free.
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 conditonal loaders like `complex/extendByComplex.mjs` could be defined.
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.

View File

@ -0,0 +1,19 @@
import './Complex.mjs'
import * as complex from './complex.mjs'
/* Add all the complex implementations for functions already
in the instance:
*/
export default async function extendToComplex(pmath) {
pmath.install(complex)
for (const name in pmath._imps) {
const modulePath = `./${name}.mjs`
try {
const mod = await import(modulePath)
pmath.install(mod)
} catch (err) {
// Guess it wasn't a method available in complex; no worries
}
}
}

View File

@ -0,0 +1,13 @@
import assert from 'assert'
import PocomathInstance from '../PocomathInstance.mjs'
describe('PocomathInstance', () => {
it('creates an instance that can define typed-functions', () => {
const pi = new PocomathInstance('dummy')
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)
assert.throws(() => pi.add(1), TypeError)
})
})

View File

@ -3,9 +3,11 @@ 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'
const bw = new PocomathInstance('backwards')
describe('A custom instance', () => {
@ -38,4 +40,18 @@ describe('A custom instance', () => {
assert.deepStrictEqual(
pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1})
})
it("can selectively import in cute ways", async function () {
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:
assert.strictEqual(cherry.add(3, 4, 2), 9)
assert.deepStrictEqual(
cherry.add(cherry.complex(3, 3), 4, cherry.complex(2, 2)),
math.complex(9,5))
assert.strictEqual('subtract' in cherry, false)
assert.strictEqual('negate' in cherry, false)
})
})