Yet another math core prototype for a possible future of mathjs
Go to file
2024-09-29 13:48:06 -07:00
src feat: Narrow tsc typing of operation dependencies/implementations 2024-09-29 13:48:06 -07:00
.gitignore feat: Narrow tsc typing of operation dependencies/implementations 2024-09-29 13:48:06 -07:00
LICENSE Initial commit 2024-09-21 17:55:51 +00:00
package.json5 feat: Narrow tsc typing of operation dependencies/implementations 2024-09-29 13:48:06 -07:00
pnpm-lock.yaml feat: Narrow tsc typing of operation dependencies/implementations 2024-09-29 13:48:06 -07:00
README.md feat: Narrow tsc typing of operation dependencies/implementations 2024-09-29 13:48:06 -07:00
tsconfig.json feat: Narrow tsc typing of operation dependencies/implementations 2024-09-29 13:48:06 -07:00

math5

Yet another math core prototype for a possible future of mathjs.

This project is a revision of the typocomath prototype (which was the fourth in the series picomath, pocomath, typocomath, hence the current name math5), preparing for an initial implementation of the Dispatcher engine to assemble and run the methods specified in TypeScript.

Motivations for the refactor:

  1. I observed that the .d.ts files that were being generated as a result of the TypeScript compilation step did not contain sufficient type information to see what each implementation/factory for each operation of the resulting math module would do. This lack suggested that the TypeScript definitions of the implementations and factories were not actually being fully typechecked.

  2. I felt that there was still a significant amount of redundancy in the implementation files. For example, in typocomath/src/numbers/arithmetic, it reiterates for every arithmetic operation "foo" that "foo" implements the number signature for the "foo" operation. It seemed like it would be preferable to specify that this module is for the "number" type fewer times, and not have to mention the operation name for each operation twice.

  3. I did not love the creation of aliased operation names like "addReal" that were actually implementations of the operation "add" but with different signatures. I found that mechanism confusing.

You can verify that the new code compiles and generates implementation information by cloning the repository and then running pnpm install and pnpm go.

Outcomes of the refactor, corresponding to the motivations:

1a. You can browse the generated build/**/*.d.ts files to see that they now contain full, detailed type information on every implementation and factory, including the exact types of the dependencies.

1b. The TypeScript compiler now correctly detected (which it had not done in typocomath) that the intermediate real square roots in the complex sqrt implementation might be used even though they had come out to NaN. This outcome is direct evidence that the TypeScript compiler is now type-checking more strictly, so we are getting greater value from using TypeScript to specify the operations' behavior. (In addition, it led to adding the isnan predicate so that the code would compile.)

  1. There is less repeated information. For example, math5/src/numbers/arithmetic only mentions number twice, and only mentions each operation once.

  2. Implementations/factories are now only exported under their actual operation names, just with different signatures specified. The default name of a dependency is the name of the operation, but when you have dependencies on a given operation with different signatures, you can name the dependency arbitrarily and then specify which operation it is an instance of.

Other potential advantages of the refactor:

  • Assembling the implementation specifications (the main task of which is resolving and injecting dependencies) into a running math engine could potentially work by parsing the .d.ts files as generated; we would not necessarily need to instrument the typescript code with macros to generate the additional information needed to correctly assemble the factories.

Some disadvantages of the refactor:

  • The presentation of the code is slightly more verbose in places. The primary cause of this is the switch to a "builder" interface for collecting implementations/factories, as advised by TypeScript guru jcalz in order to get narrower type inference as desired. So for example in src/Complex/arithmetic, every factory (a "dependent implementation", as opposed to an "independent" one that has no dependencies) is wrapped in its own call to dependent(dependencySpecifiers, factories). And that whole chain of dependent calls has to be kicked off with a call to implementations() and wrapped up with a call to ship(). Of course, the names of those functions could be changed, but it appears that currently there is no way to avoid these wrappers if we want TypeScript to do narrow type inference/typechecking.

  • When one module is providing multiple implementations for the same operation, but with different signatures, it must export multiple of these bundles of implementations generated with an implementation(). ... .ship() seequence, because each bundle can only contain an operation once. The names of these bundles are arbitrary. I think this artificial division is a little cumbersome/confusing. See src/Complex/arithmetic for an example, in which there is a default export with the "common" signatures for operations, and a mixed export with variants for add and divide that operate on a Complex and an ordinary number.

  • The notation for the desired signatures for dependencies can still be a bit arcane/cumbersome. It's very simple when the desired dependency consists of the common signature for that operation. But for more unusual situations, it can become intricate. For example, in src/Complex/arithmetic, the absquare (absolute value squared, an operation needed to define division and square root) factory needs as a dependency the addition operation on the return type of the absquare operation on the base type of the Complex number. This has ended up being specified as:

add: {sig: commonSignature<'add', CommonReturn<'absquare', T>>()}

which is a bit of a mouthful. It's possible that better utilities for expressing desired signatures could be devised; I'd want to wait until we had collected a larger number of use cases before trying to design them. (If this absquare case is essentially a one-off, it doesn't really matter if it is a bit elaborate.)