198 lines
No EOL
11 KiB
Markdown
198 lines
No EOL
11 KiB
Markdown
# nanomath
|
|
|
|
Sequel to pocomath as a prototype for mathjs overhaul
|
|
|
|
The main themes of nanomath as compared to its predecessor are:
|
|
|
|
1. Settle on a code organization, rather than merely demonstrating that
|
|
many such organizations are feasible.
|
|
|
|
2. Settle on a notation for (possibly generic) implementations, return
|
|
types, and dependencies. There have been numerous working notations
|
|
in previous prototypes, both in JavaScript and TypeScript, but
|
|
nearly all of them have suffered from over-complication of the
|
|
notation to handle tricky cases like `absquare` for real and complex
|
|
numbers. This prototype will attempt to go "back to basics" with a
|
|
straightforward JavaScript notation, using type objects and functions,
|
|
rather than a string-based type notation. However, as a design principle,
|
|
we wish to keep the type system TypeScript-compatible, and as a way of
|
|
flagging/enforcing this, we expect to write the modules implementing
|
|
the types of the nanomath type system in TypeScript. Their transpilation
|
|
into JavaScript will provide .d.ts files; some of the information
|
|
necessary for a tool to write out an overall .d.ts file for a nanomath
|
|
bundle will be gleaned from those .d.ts files.
|
|
|
|
3. Develop a unified TypeDispatcher engine that natively handles generic
|
|
implementations, rather than the two-tiered use of typed-function
|
|
in pocomath. Note this is precisely the point that the math5 TypeScript
|
|
prototype reached, so hopefully this part of the effort could easily be
|
|
ported if a decision were taken to return to something more highly
|
|
typescript-integrated.
|
|
|
|
## Code organization
|
|
|
|
The pocomath prototype showed that we could have one-file-per-implementation, or
|
|
multiple-files-per-implementation, and that we could group implementations by
|
|
type supported or by function category. The intermediate bundles could either
|
|
be by objects of objects with implementation leaves, or by "Pocomath instances"
|
|
which could then be merged. But it remained entirely agnostic about
|
|
which way to go.
|
|
|
|
This prototype aims to select and demonstrate a particular, practical code
|
|
organization that could scale to the magnitude of mathjs 14 and beyond, based
|
|
on the following observations and principles.
|
|
|
|
1. There is a desire to provide leaner custom bundles with functionality
|
|
tailored to a particular use of mathjs. In other words, we would like
|
|
to modularize mathjs, and make it easy to provide add-on pieces for
|
|
mathjs separately.
|
|
|
|
2. There is a core of mathjs that just deals with numbers. There could
|
|
reasonably be an extended core that adds just JavaScript native types;
|
|
the string type comes with its own additional collection of functions.
|
|
Other types are add-ons to these, like Complex, Fraction, Matrix, and
|
|
BigNumber. Some of those add-on types come with their own cadres of
|
|
functions, especially Matrix and Complex.
|
|
|
|
3. There are "generic" functions that can be implemented for any type based
|
|
just on other functions (such as 'sum' from 'add'); their code
|
|
should not be tied to any type.
|
|
|
|
4. There are categories of functions in mathjs that are of general interest
|
|
for almost any purpose: arithmetic, logical, relational, and utils
|
|
functions. Other categories are conceptually add-ons that each
|
|
have a more limited arena of interest: expression, bitwise, algebra,
|
|
combinatorics, geometry, numeric, probability, set, signal, special,
|
|
statistics (although I personally would move from this category max and min
|
|
to relational functions, and cumsum, sum, and prod to arithmetic), and
|
|
trigonometric.
|
|
|
|
5. When adding a new type, the "baseline" is to implement the general-interest
|
|
functions -- any time the type is used, all of those categories of functions
|
|
for it will be desirable. On the other hand, when adding a new collection
|
|
of functions, presumably of special interest, the typical approach would
|
|
be to provide implementations for all relevant types.
|
|
|
|
6. We want to make some categories of functions (perhaps even existing ones)
|
|
independent of the main mathjs package, and some types (perhaps even
|
|
existing ones) independent of the main mathjs package.
|
|
|
|
To set terminology, we will call the general-interest categories as delineated in
|
|
point 4 the "basic" categories, and other categories "special" categories.
|
|
We will call types and function categories to be included in the main mathjs
|
|
package "standard".
|
|
|
|
With those preliminaries in mind, here's the proposed code layout within src
|
|
for the main package. Note that each file will just export implementations,
|
|
and each directory will have an index.js collecting all of the material in
|
|
that directory and re-exporting it "one layer deeper" for avoiding name
|
|
conflicts.
|
|
|
|
* Have a top-level directory called `number` with a file for each basic
|
|
category, exporting the number implementations for all of the
|
|
functions in that category.
|
|
|
|
* Have a top-level directory called `generic` with a file for each relevant
|
|
category, exporting all type-independent implementations for functions
|
|
in that category.
|
|
|
|
* have a top-level directory `builtin`, with subdirectories `scalar` and
|
|
`collection`, for JavaScript built-in types. The `scalar` subdirectory
|
|
will have further subdirectories `nullish`, `boolean`, `bigint`, and
|
|
`string`. The `collection` subdirectory will have further subdirectories
|
|
`Object` and `Array`, and to the extent we support them, `TypedArray`,
|
|
`Set`, and `Map`. Within each of the single-type subdirectories, we will
|
|
have a file for each relevant category. There may also be a need for some
|
|
mixed-type subdirectories, and we also need to decide where to put
|
|
implementations for such operations as an array plus a scalar. Hopefully
|
|
most of these sorts of thing can be handled generically via some sort of
|
|
broadcast method.
|
|
|
|
* For every other standard type, there should be a top-level directory,
|
|
with files for each basic category and for any special categories
|
|
particularly associated with that type (e.g. the complex functions for
|
|
the Complex type). As an exception, the Node type and subtypes should remain
|
|
within the expression directory described below, since they are needed if
|
|
and only if one is working with MaJE. The implementations for this standard
|
|
type for other special categories should be left to those _categories_.
|
|
|
|
* For each standard special category, there should be a top-level directory.
|
|
It should have a file for each argument type relevant to the category.
|
|
The rationale for the inversion from the hierarchy for the basic categories
|
|
(which have the type at the top level, and category within) is to
|
|
facilitate loading only the categories you want for a given application.
|
|
Unfortunately, this means if you want maximal tree-shaking for just a few types,
|
|
like say the built-in ones, you will have to load the `number` and `builtin`
|
|
directories, and then also the algebra/number, algebra/builtin,
|
|
trigonometry/number, trigonometry/builtin, special/number, special/builtin,
|
|
etc. etc., directories. If tree-shaking is less of a concern, you should
|
|
be able to simply load algebra, trigonometry, special, etc., and the
|
|
implementations for types that aren't being used should simply be ignored.
|
|
|
|
It's also worth thinking about how add-on packages that extend mathjs should
|
|
be laid out. I would expect them to come in two main flavors: those that add
|
|
a type to mathjs, like 'Chroma' or 'Unit' (if it truly becomes independent),
|
|
and those that add a new special category, like 'numberTheory' or 'signal'
|
|
(if it becomes independent). If a new type is specified, the package should
|
|
have a directory for that type, with files for each basic category, and then
|
|
additional files for each other standard category relevant to the type. If it's a
|
|
new category, it should just have files for each type.
|
|
|
|
There's a tricky bit about what to do to support extension categories on
|
|
extension types: For example, if Unit ends up as independent, and numeric ends
|
|
up as independent, where would the Unit implementation for solveODE go? Well,
|
|
to be parallel with the arrangement above of standard types and categories,
|
|
the type package should only implement standard categories and any special
|
|
category closely associated with that type. So the implementation should go
|
|
in the independent category package, in this hypothetical case the numeric
|
|
package, leading to the conclusion that a non-standard category package
|
|
may have files for non-standard types that it "knows about" -- of course,
|
|
there's no way for it to anticipate all possible future types, except insofar
|
|
as it can be written generically to operate on any type through the
|
|
basic-category functions as building blocks.
|
|
|
|
## Type objects
|
|
|
|
A reasonable place to start is the objects registered as types in pocomath.
|
|
For scalar ground types (where "ground" is as opposed to "generic"), these
|
|
objects are quite simple: they have just a membership test and some conversions
|
|
from other types. We will also want to allow them to have a 'subtypeOf' property,
|
|
at least for the sake of the Node hierarchy. The test for a subtype can
|
|
be assumed to only be called when the supertype's test has succeeded.
|
|
I think that's about it, and then e.g. this `number` type object can be
|
|
exported as `number` and just used in implementation signatures directly.
|
|
|
|
Things become a bit more complex for generic types. They must be represented
|
|
as functions that will return a type object (or further generic type
|
|
function, if they are called with generic type(s) for one or more of
|
|
their arguments). These functions should have some similar properties: a `base`
|
|
test that determines if an entity belongs to some instantiation of this type;
|
|
a membership test generator that given membership tests for the type arguments
|
|
to this generic type, returns a membership test for the instantiated type; and
|
|
some parametrized conversions from other types.
|
|
|
|
We will need some special type objects and functions. We need a Type Parameter
|
|
type that can represent any type, but in type testing, sets what type it found
|
|
in a type assignments object that gets carried along with the type testing.
|
|
We need a Union generic type. And we either need a Tuple generic type, or
|
|
else we need to support the convention that an array of types is the type of
|
|
an array of elements, each of the corresponding type. And types will need
|
|
hashes, to look them up in implementation maps. These need not necessarily be
|
|
explicit in the type objects, they can be assigned by the NanoInstance when the
|
|
types are registered.
|
|
|
|
## TypeDispatcher
|
|
|
|
The plans are a bit sketchier here, since pocomath is the only prototype so far
|
|
that had a working dispatcher, based on using two tiers of typed-function.
|
|
But the basic idea is for each dispatchable "method" to have its "specs" and
|
|
its "behaviors." The behaviors will be a map from signature hashes
|
|
to immediately callable functions. The specs will be implementations, that may
|
|
have dependencies, generic type parameters, union type parameters, etc. The
|
|
basic idea is that the implementation of a method will take the hash of its
|
|
actual arguments, and look it up in the behaviors. If it finds a behavior,
|
|
it will call it on its arguments. If not, it will go through its specs (not sure
|
|
if the order will matter), looking for one that matches. If it finds one,
|
|
it will instatantiate it as to any generic parameters, and supply any
|
|
dependencies it might have, resulting in a behavior, which will be recorded
|
|
in the map of its behaviors and then called. |