math5/README.md

110 lines
5.7 KiB
Markdown
Raw Normal View History

2024-09-21 17:55:51 +00:00
# math5
Yet another math core prototype for a possible future of mathjs.
This project is a revision of the
[typocomath](https://code.studioinfinity.org/glen/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.)
2. There is less repeated information. For example,
math5/src/numbers/arithmetic only mentions `number` twice, and only mentions
each operation once.
3. 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](https://stackoverflow.com/questions/79025259) 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.)