We use [mocha](https://mochajs.org/) as the test framework, as it is the tool used by mathjs and we would like to make tests as similar as possible. However, to tighten the linkage between source code and tests, we adopt a somewhat different file organization: unit tests for a given source file `blah/foo.js` are in `blah/__test__/foo.spec.js`. To run all unit tests, execute the script `pnpm test`. Resolves #3.
218 lines
No EOL
12 KiB
Markdown
218 lines
No EOL
12 KiB
Markdown
# nanomath
|
|
|
|
Sequel to pocomath as a prototype for mathjs overhaul
|
|
|
|
### Set up
|
|
|
|
1. Install the `pnpm` package manager on your machine.
|
|
2. Clone the repository `https://code.studioinfinity.org/glen/nanomath.git`
|
|
and change directory into its top level.
|
|
3. `pnpm install`
|
|
4. `pnpm test` runs the tests.
|
|
|
|
## Design philosophy
|
|
|
|
Note the rest of this README lays out the initial structure and design
|
|
goals for the nanomath prototype, and remains a general guide for its
|
|
development. Some specific details have been adjusted, and this document
|
|
has not necessarily been kept up-to-date. See the
|
|
[project wiki](https://code.studioinfinity.org/glen/nanomath/wiki) for
|
|
the most recent details; where the wiki and this README differ, the wiki
|
|
is presumed to be authoritative.
|
|
|
|
### Themes
|
|
|
|
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. |