Sequel to pocomath as a prototype for mathjs overhaul
Find a file
2025-03-30 20:00:07 -07:00
src feat: add type definition and other function categories for number 2025-03-30 20:00:07 -07:00
.gitignore doc: Initialize pnpm and flesh out README 2025-03-29 16:39:29 -07:00
LICENSE Initial commit 2025-03-29 16:57:23 +00:00
package.json5 doc: Initialize pnpm and flesh out README 2025-03-29 16:39:29 -07:00
pnpm-lock.yaml doc: Initialize pnpm and flesh out README 2025-03-29 16:39:29 -07:00
README.md doc: Initialize pnpm and flesh out README 2025-03-29 16:39:29 -07:00

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.