-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Design question] I don't like factory
. Why don't we just use modules?
#1975
Comments
factory
is terrible. Why don't we just use modules?factory
. Why don't we just use modules?
Good question. Thanks for asking. One side-note about the screenshot you shared for To explain the context: What I wanted to achieve with mathjs is an environment where you can do calculations with mixed data types, like multiplying a regular The solution that we have in mathjs now is a combination of two things:
The dependency injection indeed complicates the code hugely. If you or anyone can come up with a simpler approach I would love hear! Maybe we can do something smart during a compile/pre-build step instead or so. |
Thanks for your reply!
You mean I should bundle
I think I understand
Hmm... I was convinced that if was possible to add call signatures to a typed function, but I couldn't find that anywhere in the examples. Okay, I take back my last statement, maybe there are things to improve in const fn4 = typed({
'number': function (a) {
return 'a is a number';
}
});
fn4.addSignature({
'number, number': function (a, b) {
return 'a is a number, b is a number';
}
})
fn4(1,2) // a is a number, b is a number Then the import math from 'mathjs'
math.typed.addType({
name: 'BigInt',
test: (x) => typeof x === 'bigint'
})
math.bigint = (x) => BigInt(x)
math.add.addSignature({
'BigInt, BigInt': (a, b) => a + b
})
math.pow.addSignature({
'BigInt, BigInt': (a, b) => a ** b
})
export default math This is arguably much simpler than the original, and no compile-time magic was needed – just vanilla modules.
This one sounds a lot more difficult to achieve with modules. Generally speaking, it's impossible to make a module "unlearn" some dependency – at least without some compile-time magic. But at the same time, it seems quite impractical to manually remove dependencies on something like BigNumbers... Since more than half of the code directly mentions them, one would have to rewrite almost all functions to remove the dependency – I struggle to understand why would anyone do that. Are there any more practical examples of where such "unlearning" might be used? |
Hm, good point. We should probably not expose So far, typed-function is created in an immutable way. So you can merge typed functions like The world would be simple if we have a single mathjs instance and can extend functions there like you describe, and also change the config there. However, suppose you're using two libraries in you application, library A and library B, and both use mathjs. If those two libraries both need different config and extend functions in some ways, they would get in conflict with each other. Therefore, I think it's important that you can create your own instance of mathjs with your own config and extensions, and the global instance should be immutable.
I suppose you mean loading light-weight functions with just number support instead of the full package? Reasons are performance (typed-function does introduce overhead) and bundle size for the browser. Currently, functions like |
Huh, I didn't realize mathjs is immutable now and making it mutable could break some real world code... This makes things infinitely more complicated. After some time of listening to music and thinking hard, I've come up with this design idea. It's probably still full of holes, so if you notice something that wouldn't work, doubt that something is a good idea, or simply don't understand my explanation, please do comment on that. It's definitely more complicated than “just exporting and importing” as I initially hoped, so please be patient with my explanation 😅️ Proposal v0In any mathjs bundle, according to this proposal, there would always be two abstract types:
The subtypes of these can be modified (you can remove some, or add your own), but these two abstract types shall always remain. In any mathjs bundle, the
If a user wants to add their own Real type or Scalar type, they should provide these methods – for the best results they should provide all of them. Ordinary methods (like Pseudo-code to showcase this convention: // file: multiply_DenseMatrix.js
import { typed } from "../core/typed.js"
import { DenseMatrix, pointwise } from "../type/matrix/DenseMatrix.js"
export const multiply_DenseMatrix = typed('multiply', {
'Scalar, DenseMatrix': (a, B) => pointwise(B, e => this.multiplyScalar(a, e)),
'DenseMatrix, DenseMatrix': (A, B) => DenseMatrix([ /* actual multiplication code */ ])
}) There are various pre-made bundles like This is a pseudo-code implementation of export create(methods, config = {})
{
const math = { config }
for (fname of keysOf(methods))
{
if (!fname.contains('_'))
{
math[fname] = bind(methods[fname], math)
}
else
{
const name = fname.split('_')[0]
const method = mergeAllOverloads(name, methods)
math[name] = bind(methods, math)
}
}
return math
} The result of How a user uses this new API
Pros & Cons
|
Mathjs currently has a complicated hybrid: the lowest building blocks, functions, are immutable. On higher level, you can create a mathjs instance and change configuration. That will result in all functions being re-created with the new configuration. Interesting idea to separate required core functions (like So if I understand your idea correctly, when creating a mathjs instance, the functions are simply bound to new function add (a, b) {
return a + b
}
function sum (values) {
let total = 0
values.forEach(value => {
total = this.add(total, value) // <-- here we use 'this'
})
return total
}
const instance = {}
instance.add = add.bind(instance)
instance.sum = sum.bind(instance)
console.log('sum', instance.sum([1, 2, 3])) Relying on a It would be nice if it would be possible to export already bound functions which can be used directly, in such a way that tree-shaking would work and you should be able to re-bind the function to a different context later. Something like this works (but having to wrap it is still ugly): // because we import all dependencies here, tree-shaking and cherry picking a function just works
import { add } from './add.js'
const sum = (function () {
// default binding of all dependencies
this.add = add
return function sum (values) {
let total = 0
values.forEach(value => {
total = this.add(total, value)
})
return total
}
})() Which can be used like: import { sum } from './sum.js'
console.log('sum', sum([1, 2, 3])) And you can bind it to a different context like: const instance = {}
instance.add = // ... some different implementation
instance.sum = sum.bind(instance)
// now, instance.sum uses the new add function I think though that it is not possible to bind Really interesting to think this through 😎 |
Bump! I'm interested in continuing this discussion and possibly making a few prototypes to test the possibilities! Regarding your last comment: I think I have an idea how to solve this. I'll assume we'll be using TypeScript, but the idea would work without it too.
// file: matrix/essential.ts
export { matrix } from './matrix'
export { add } from './add'
export { multiply } from './multiply'
export { transpose } from './transpose'
... // file: matrix/solve.ts
import * as core from '../core'
import * as essential from './essential'
import { createExtras } from '../utils/createExtras'
// non-essential functions; for the sake of example, only one of them is made extensible
import { lup } from './lup' // extensible
import { usolve } from './usolve' // not extensible
import { lsolve } from './lsolve' // not extensible
export function solve(this: core & essential, M: Matrix, b: Vector) {
const extras = createExtras(this, { lup }) // only creates the object once, then caches it
const { L, U, P } = extras.lup(M)
const c = usolve(U, b)
const d = lsolve(L, c)
return d
} Then if you cherry-pick import { create } from 'mathjs/custom'
import * as core from 'mathjs/core'
import * as matrix from 'mathjs/matrix/essential'
import { solve } from 'mathjs/matrix'
const fn = { ...core, ...matrix, solve, lup: ()=>{ my code }, usolve: ()=>{ my code } }
const math = create(fn)
math.lup === fn.lup // true
math.usolve === fn.usolve //true
math.solve([[1]], [1])
// uses your custom lup
// but doesn't use your custom usolve What do you think about the proposal? Should I make a prototype repo to test this in practise? |
An argument for designing a new architecture: In the current system, creating one function means modifying five different files in different parts of the codebase. I think this number should go down :^) |
Your proposal can be interesting @m93a . I guess a main difference with the proposal I made before (see #1975 (comment)) is that you're relying on a I'll reply on the other issues in mathjs that you commented on soon but I'm not managing to keep up with all the issues at this moment. |
For a working tiny prototype using many of the ideas in the discussion of this issue, but which does not use any manipulation of "this" and takes a more simpleminded approach to bundle-gathering than the "areas" and "createExtras" later in the conversation, but nevertheless seems as though it may well be scalable to the size of mathjs, see: https://code.studioinfinity.org/glen/picomath |
Oohh, I'm really curious to have a look at your PoC 😎 ! Will be next week I expect though. |
Great. I also went ahead and (in an additional |
Unfortunately the picomath proof-of-concept relies on a possible version of typed-function in which typed-functions are mutable (e.g. they can have additional signatures added to them after initial creation, without changing object identity). That is possible, but the experiments in josdejong/typed-function#138 indicate that it comes at too heavy a performance cost. So I think that approach is a dead end for now |
Thanks for adding the pointer here to the experiments and benchmarking that you did in this regard. That was indeed quite a bummer. We have too keep experimenting and trying out new ideas :) |
Yes, I have a new concept: in typed-function v3, we allow implementations to flag that they refer to another signature of themselves (or their entire self). I am imagining a variant of typed-function in which individual implementations can flag that they refer to a signature of any other typed-function (or the entire other typed-function) as well. Then we just load modules gathering up all implementations of all typed-functions (creating a huge directed graph among impementations, that is hopefully acyclic). But no actual typed-functions are instantiated yet, because we don't know if any will get more implementations as we load more modules. Then just before we actually start to compute (maybe triggered by the first attempt to call a typed function, rather than define one), the whole web of definitions is swept through in topological sort order, finally instantiating all of the typed-functions, so that all of their references have been instantiated as well, and they can all be compiled down to code that doesn't need to call functions through possibly changing variables, which is the slow operation. This would have the likely effect of slowing down initialization (but perhaps it can be done incrementally so that this burden is spread out) but hopefully keeping computational performance once everything is initialized as high or higher than it currently is. This description may be a bit vague; as time permits I will do a "pocomath" proof of concept and post here. |
OK, the new proof-of-concept Pocomath is working. It does everything the original picomath did, and more. (I have not implemented the lazy-reloading of functions when a configuration option changes. I think it's quite clear from what's there that Pocomath can easily handle this capability, but I would be happy to do a specific reference implementation of it if anyone would like.) Specifically, it uses the current, non-mutable, typed functions of typed-function v3, but allows gathering of implementations solely via module imports, it has no factory functions, and it should easily allow tree-shaking. To show that this latest architecture makes it very easy to implement new types and more generic types, I have implemented a bigint type in Pocomath and in combination with the Complex type there it gives you Gaussian integers (ie. Complex numbers whose real and imaginary parts are bigints) automatically. I am very encouraged by this proof-of-concept, and would be delighted for everyone interested to look it over. I humbly submit that adopting this basic architecture would make organizing the mathjs code as desired and adding new types and functions much easier. (In particular on @m93a's criterion of the number of files you have to touch to add a new operation.) Looking forward to your thoughts, @josdejong. |
I love it @gwhitney 😎. I love the simple, public API, which I think boils down to: const math = new PocomathInstance('math')
math.install({
[functionName]: {
[functionSignature] : [dependencyNames, dependencies => function],
...
},
...
}) This is really perfect to easily compose all kind of functions and data types. The lazy collection of signatures and only build as soon as you use a function ensures there is no double work done creating typed-functions (which is relatively slow). And this way we end up with typed-functions that are the same and should perform the same as the current mathjs instance. It is amazing how litte code is needed in PocomathInstance! (of course, it's a POC, and things will grow, but still...). I think what we loose here compared to
It has factory functions, but the dependency injection now moved from the function level to the signature level. That is an interesting take, and the magic ingredient of pocomath I think, making this composable solution possible and very straightforward! We can discuss again about whether to use the array notation About tree shaking: this indeed works, but with this approach we leave it up to the user to import all stuff needed to satisfy all dependencies. The generic I see the use of dynamic imports in One practical issue: Windows cannot handle two files with the same case-insensitive name: |
OK, there was one major bug in Pocomath (which I have fixed): each PocomathInstance needs to have its own typed-function instance (if for example no functions on Complex numbers have been installed in one instance, it should have no notion of a Complex type). So for the purposes of the proof-of-concept I just made the "Types" operator of a PocomathInstance be interpreted specially as the list of types to be added to the typed-function universe of the instance. That all seems to work fine, although in a full production version there could be a somewhat more elegant way of expressing the Type dependencies. This work surfaced one really desirable extension to typed-function and one outright bug in it that I was able to work around; I have filed them in its repo (josdejong/typed-function#154 and josdejong/typed-function#155). I've also implemented a bunch of changes/extensions to Pocomath based on your great feedback, thanks! Specific comments interpolated below:
Yes, that's captured very nicely.
Well, to be clear, if you install some signatures to math.add, say, and then call it, and then install some more, and then call it again, two typed functions will be constructed and the first will be thrown away when the second is made.
I agree. Once all the installs are done and the functions you need are instantiated, the resulting collection of typed functions should be as performant as current mathjs, with the potential to do slightly better if the feature allowing direct reference to a specific signature is easy enough to use and applicable enough that it gets substantial use (once implemented).
I do think this is a fair amount of what would be the core of a full production version. It is pleasant that there doesn't seem to be a lot of infrastructure beyond what typed-function provides.
Ahh, this excellent comment gave me the idea to use getter functions to produce the typed-functions instead of temporary self-removing values for the operations defined in the instance, so that nobody could be storing a fake function that doesn't do anything. That doesn't eliminate your point, though; but on the other hand, math.js currently has this issue: If you save math.add from math.js and then do an import that overrides add, your old copy of math.add is stale and won't do the new behavior.
Yeah, because in JavaScript you can teach an old function a new trick, but only at a high price in speed.
OK, I understand that perspective. But you don't have to think about writing factory functions, you just export your semantics decorated with what they depend on.
I agree there are lots of possible notations and the dependencies could be optional. I implemented a bunch of different options (I don't think we would actually want to allow them all in practice) and used a different one for each of the types that are currently implemented in Pocomath so that you could look at the possibilities visually and see which one or ones you liked to have in the end.
Yes, I am slightly worried that functions that have a long list of dependencies will be a pain, but the basics are pretty smooth so I am hopeful that won't be too bad.
As you probably saw right now, you can already just say you depend on
which looks a bit ugly/cumbersome. One thought I had is that if you said you depended on
which looks cleaner to me. If you have reactions/thoughts on this scheme or other approaches to make the notation convenient, I am very interested.
I've implemented two possibilities for this and given examples in Pocomath. First, for generic functions like subtract, we could have corresponding 'concrete' modules that also import enough other stuff that you can actually run subtract, at least on numbers, say. So if you only load 'concrete' modules you'll always be in a state that runs, at least in a minimal way. Second, I added a function to the core that (dynamically) loads all of the files you need to compute all of the functions you've installed so far on a list of types you specify. (We could add a diagnostic parameter that would also make it display all of the modules you need, so you could at least cut and paste to make a static import for what you desire.)
And yes, as a third but I think the most usual method, the idea was that modules like
Are you sure? webpack and vite both say they work with dynamic imports. But I have exactly zero experience with building/bundling JavaScript, so "working" may mean something different than I think...
Right, and at least we can always enhance the dynamic loaders with options to emit the corresponding list of static imports you'd need to make a static version, which would then presumably lead to a nice lean bundle.
Done, and especially it made sense to move these files to distinct places when switching to the 'Types' pseudo-operation to specify the types that some functions would need to have around. I also added a nifty little check against future case-collisions. In sum, do you think there's potential here for a route toward a reorganization of actual mathjs that will make it easier to add further functions and types (and possibly to supply other collections of functions and types as somewhat independent add-ons to mathjs)? What would be the next features/facilities you'd like to see Pocomath grow to have to be sure it could overcome the hurdles for full production use? Thanks very much for your encouragement and feedback. |
Update: after implementing all of the various possibilities for dependency/implementation notation yesterday, an idea for how it might be very nice to do occurred to me, which was encouraged and refined by a StackExchange answer by James Drew (see https://stackoverflow.com/a/41525264). Under the new scheme, the generic implementation of subtract becomes:
and that's it. PocomathInstance can obtain the names of the dependencies from the keys in the destructuring parameter of the (outer) function -- thanks to James Drew's trick -- plus that destructuring makes the resulting referred-to functions super easy to use in the body of the implementation. For signature-specific references (when they are implemented), the notation will be:
This notation just seemed so clearly better than all the other options that I went ahead and switched Pocomath to use only this one. (You can rewind to the previous commit if you want to look at all the other options.) Its only tiny drawback is that for functions without any dependencies, I could not find any approach to make the initial |
Update no. 2: I wasn't quite happy with the way types were being specified, so I fixed that up a bit and added the feature that signatures that use unknown types are simply left out of the typed-function. That way you can install an operation with definitions for lots of types, and the ones for (currently) undefined types will be ignored, but the operation will be rebundled whenever that type becomes defined. This would be convenient for operations where the definitions for all types are collected into a single file, as opposed to the type-segregated approach I have been using so far; likely eventual uses of this approach might mix the two styles of organization. There is one slight concern about this relaxed approach to unknown types, that it won't catch typos in type names. i think those could be caught another way, I'm filing that as an issue in the Pocomath repository. |
Update no. 3: I implemented reference to specific signatures (of another function or of yourself). Most of the work revolved around the fact that Pocomath is lazy about types not yet being defined, while typed-function insists they all be there already, but it wasn't too bad. I also realized that lazy reloading of functions upon a config change came almost for free with the basic framework, so I added that. Used these things to implement sqrt for number and Complex types. The definition should be easily extensible to bigint and the resulting Gaussian integers, I will check tomorrow. But I think most all of the features that would be needed to extend this PoC to a fulll mathjs-size production version are now there. |
Update no. 4: There was a remaining problem with the type system. Since all types were being put as keys in the value of the single identifier There is an outstanding architecture issue, which you can see at https://code.studioinfinity.org/glen/pocomath/issues/29 about a similar phenomenon with merging different signatures of an operation. I would be happy to have any feedback you might have about a preferred route to go with this. All else equal, I am leaning toward option 4 in that issue. From my current perspective, when that point is ironed out, the architecture for Pocomath is reasonably developed and looking scalable. Thanks, and looking forward to whether this could have a real positive impact on mathjs. |
Thanks for the updates.
Indeed after importing a new signature, all dependent functions need to be re-created with typed-function. In general I think we do not need to worry. According to the load.js benchmark, creating all typed functions of mathjs takes like 40ms right now. And typically you import all you want upfront. We can just document the behavior so people understand how to optimize. Good points on how to satisfy dependencies, with something like "concrete modules". The dynamic loading is indeed cool, though it can only be used in a node.js environment I think. In the current mathjs, I created scripts to generate files like "dependenciesAdd.generated.js", we could do something similar.
Let's just try it out to know for sure :). A dynamic import like
Yes, I like this approach. I would love to have it at the core of mathjs (and get rid of some complex, old backward compatible mechanisms too).
Let me think. Some things that pop in my mind:
About the dependency injection: that new notation is really interesting. The destructuring is indeed very neat. What I am wondering though is: in my head it should be possible to either provide a regular function if you do not need to inject dependencies, or provide a factory function. With the new notation like I do love your idea to use strings like I still have to check out the latest version of pocomath, will keep you posted. |
I am pretty sure it also works in a browser if one makes all of the files available at the relative URLs corresponding to the path names being imported.
Absolutely.
Agreed.
Great news, makes me feel like the time I have spent pondering this puzzle has been worth it.
Already done in the latest revision,
OK, there are already enough functions to make a Chain type, will add a PR for it and let you know when it's implemented.
Should actually have a zero or positive impact, I think If you want to point me to a particular benchmark or two in the mathjs collection of benchmarks I will implement the same computation in Pocomath so we can have a race ;-)
Trying to see if I understand your concern. Are you worried about, say, the
Indeed, with the current architecture I could not find a way to make this work. Currently, the price of this "accumulate implementations and regenerate typed-functions whenever necessary" architecture is that you have to say:
I don't think a dozen or so characters is a big price to pay, and the upside is that for any implementations that have no dependencies (which frankly I think are a fairly small minority), we are explicitly stating that they are dependency-free with the initial That said, if you would like I could certainly add a notation something like
for when you want the implementation just to be a plain function. We can use any value for the signature key in this case that you want that is not a function entity at the top level itself, even
or the infrastructure is going to blow up. Or is your concern that for some operations you don't want a typed-function at all, just to assign a regular JavaScript function that will be used for all arguments, like say a logger that writes whatever it gets to a log file or the console or something? I could easily support something like
in a Pocomath module and then when that module is installed in a PocomathInstance pm, the given function becomes the definition of pm.logger (replacing any previous such definition and causing any typed-function implementations that depend on the logger to be lazily re-bundled as needed). The point is that the "usual" value of an identifier in a Pocomath module is a plain object mapping signatures to implementation producers, so the infrastructure can notice a
Glad you like it. I find it more convenient than typed-function's current notation. OK, sounds like this PoC is well on its way. I have some issues on it that you can see that I am going to implement, to which I will add the Chain type, as well as one or more of the "plain function" options discussed above if you think they would be useful in addition to the "standard" implementation-producers currenty used. I will check back in here when all that's done and we can plan next steps. |
I've been looking through the latest version of pocomath. I have to say I really love how it works out. It starts itching to get this approach implemented for real in
Yes you indeed understand what I mean. I've been thinking more about this, that it is now always necessary to pass a factory function for every signature. In cases like
That was a second concern actually :). The use case I'm thinking about is that it is very powerful if you can just import some statistics or numerical library from npm straight into pocomath. These are no typed-functions. Here is a mathjs example demonstrating that in mathjs: https://github.com/josdejong/mathjs/blob/develop/examples/import.js One other thought: in mathjs, over the years, their grew a need to have a distinction between functions that operate on scalars vs the functions that accept matrices too. So now you have |
Sure, I am perfectly happy with a "wait and see" approach on an "adapter for a plain-function implementation".
Yes, it does seem powerful to import external libraries. I will add an issue for installing ordinary JavaScript functions to the list I am implementing in Pocomath.
This is an excellent point. It is not yet clear to me if the reasons that addScalar is needed in the current architecture will arise with a Pocomath-based architecture. One question that has been kicking around in my head is why is there a Matrix type in mathjs in addition to Arrays? I assume the primary answer is so there can be both DenseMatrix and SparseMatrix. But it has also been kicking around in my head that another item that makes Matrix powerful is that it is (or at least can be) type-homeogeneous, i.e., all entries are the same type. And if this is part of the sauce, then it seems to me that it would be pleasant if It is indeed possible to build a concept of template types on top of typed-function as it exists (or eventually it could perhaps be incorporated internally therein). Then we might have something like (in pseudo-code):
(Hence, an item to implement templates is next up on my list of things to try in Pocomath.) I think an approach like this might make it unlikely that the need for a dependency on On the other hand, if we find ourselves needing a way to refer to "add for scalar types", maybe to deal with potentially inhomogeneous collections like Arrays, I think it would be possible to introduce esentially "typedefs" into Pocomath in which we install a type 'scalar' into an instance with a designation that it simply means (After all, the current version of Pocomath always filters implementations to include only those that mention a defined type, in order to allow operation-centric files like mathjs has where a single operation is defined for numerous types, but only the implementations for types that have actually been installed in the instance are employed. Pocomath is trying very deliberately to be completely agnostic as to how operations and their implementations are organized into source files, even though the demo so far uses one operation for one type per file. I plan to add an example of many operations for one type in a single file, and I will also add an example of one operation for many types in a single file just to verify it works.) However, this feels like it may end up being a little complicated so I would definitely put this concept on the same list as plain-function-implementation-adapters, i.e., things to be implemented if the actual need arises. |
I'm not sure either, but I think the same issues would pop up, it helps for performance and also better error messaging to have functions that only work for scalars. There was one (quite) specific circular dependency: The reasons for a
When rethinking matrices, here are some thoughts:
That is a very interesting idea 🤔. So basically, that would allow not only to inject a function, or a single function signature, or self reference, but it would allow you to filter a set of signatures out of all of the signatures of a function and create a new, optimized function. It sounds very cool. I agree with you though, that something like this could open a lot of tricky complexities, so maybe best to keep it as an idea for now. |
On scalar functions:
I am thinking/hoping that the template implementations will be as good or better on both of these counts.
The Pocomath-style of infrastructure has no problem with co-recursion (as long as it bottoms out at some point). For example, the exact circular dependency you mention currently exists in Pocomath as it stands right now: the generic implementation of |
On Matrix and friends: |
OK, template implementations (but not yet template types) are working in Pocomath now. I've switched as many of the generic operations to use them now as I could figure out how to. Also, there's a nifty built-in implementation of |
And now I have added an example of defining the 'floor' function in an operation-centric way. Note I am not proposing that in a putative reorganization of mathjs to use a Pocomath-like core that both operation-centric and type-centric code layouts would be mixed, I just wanted to demonstrate that the Pocomath core is completely agnostic as to the organization of the code. (Except for |
Pocomath now has a working Chain type (with the methods only being chainified when called through a chain and updating when they are modified on the underlying Pocomath instance). However, the chain methods are not quite as completely "lazily" updated in that at every extraction of a method, this implementation of Chain checks whether the underlying operation has changed on the Pocomath instance. If you think that's enough of a problem for performance (as opposed to the Pocomath instance invalidating the chainified version when it updates the underlying, so that the chainified version can simply be called directly whenever it's valid, without checking for an update) to be worth it, I can modify it to work that way as well. The only cost is somewhat tighter coupling between the instance and the Chain. (Right now, the instance provides a place for Chains to store their chainified functions, since of course the underlying items that are being chainified could vary from instance to instance. So that repository has to be associated with the specific instance. But the contents of that place are completely managed by Chain. To make things even lazier, Chain and PocomathInstance would have to have shared knowledge of the format used to store chainified functions in the repository so that PocomathInstance could do the invalidation directly when it was changing one of its operations. Let me know if it seems OK for the proof of concept or if you'd like the invalidations to be "pushed" from the instance. |
Import of plain functions is working now. |
Template operations and template types are both working in the prototype (Pocomath) now. There is a type-homogeneous vector type |
And now there is an adapter that just sucks in fraction.js (well actually bigfraction.js, because I think it's clear mathjs and fraction.js should move to the bigint version) with no additional code added elsewhere in Pocomath. With that, my feeling is that Pocomath is "feature-complete" as a proof-of-concept: it demonstrates all of the aspects that I would be proposing to bring to mathjs with a reorganization along these lines. There are a couple of very minor concerns mentioned on the issues page, but I think they would easily come out in the wash in an integration with typed-function/mathjs so I don't feel they are worth running down now. So I think the only remaining "proof" the concept needs for evaluation is some benchmarks, to ensure that the additions to typed-function do not bog the system down. I believe they won't, because in fact Pocomath tries very hard to bypass typed-function dispatch as much as possible and bundle in direct references to individual implementations rather than to typed-function entry points. So please just point me to some benchmarks for mathjs that you think might be reasonable for evaluating Pocomath in this regard, and I will duplicate them in Pocomath and post the results. Thanks! |
just 😎
Yes you're right. I should better separate the different discussions (and at the same time, the discussions related to TypeScript and the pocomath architecture are a bit scattered right now).
The genererics are impressive, it makes sense. The
👍
😎 again. I how to implement the lazy or less lazy updating of functions is an implementation detail. I think we can just start with the current approach, and do some benchmarks to see if there is a performance issue in the first place, and if so try out alternative solutions.
🤯 ...Shortcut in my head... 😂
yeeees, that's where we want to go, easily import a new data type that has a set of built-in functions/methods. Fraction.js, Complex.js, BigNumber.js, UnitMath, ... Optional dependencies Benchmarks
Thanks a lot for working this concept out Glen. I really appreciate this. I think this concept addresses all the pain points of the current architecture, I really believe this is the way to go. There is one big open challenge: TypeScript support discussed in #2076. I want to find a satisfying solution for that first, before implementing this architecture for real in mathjs. |
Return type annotations are now supported, and in fact supplied for all operations, in the Pocomath proof of concept. This move the POC closer, I suppose, to the sort of organization need for a conceivable TypeScript switch/unification. The change took a while both because I physically relocated, but also because it was a very good bench for strengthening the underlying infrastructure. Type checking and template instantiations are now significantly more robust. As soon as I can I will get to the final piece of the proof of concept, namely some reasonable benchmark. I am virtually certain that building an instance with all of its dependencies satisfied will be slower in Pocomath; there's just a great deal to do as far as instantiating templates and resolving every individual implementation, which is clearly less efficient than resolving the dependencies for an entire source file all at once, which may have many implementations in it. What we're getting is a great deal more flexibility in organizing code. So in a benchmarking of bootstrapping time, Pocomath is bound to lose by a significant margin. But that's just a one-time initialization cost. The aspect I feel needs benchmarking is the performance of the resulting implementations of the operations, and there I think I can reasonably hope that Pocomath will noticeably outperform current mathjs because with the templating (or generics if you prefer that terminology), a nested Pocomath operation should perform many fewer typed-function dispatches. I will report back here when I have results. |
Hmm, I looked into this but currently the only benchmarks that do a significant amount of numerical computation are the matrix benchmarks (but reimplementing matrices in Pocomath doesn't make sense) and isPrime (but that test just calls a single typed-function that has no dependencies, so there really isn't any difference to test). So I will need to add a new benchmark on the mathjs side. I need something fairly realistic that calls a variety of basic arithmetic functions. Please does anyone following this issue have a suggestion for an algorithm or calculation I might use as a benchmark for this purpose? I could solve a quadratic and a cubic using the respective general formulas... That's reasonably attractive because the cubic goes back and forth into complex numbers. But it's fairly simplistic. Any better suggestions? |
I see the Returns is used dynamically, inside the function body. I'm not sure, but I have the feeling that that will make it hard or impossible to statistically analyse with TypeScript. What do you think? About benchmarks:
I thought I had reported about this but I probably forgot or wasn't yet happy with it yet, sorry. This benchmark that I parked in this separate branch is quite limited, it would be better indeed to have a real expression with multiple operations. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
I'm working on this PR and the current dependency system of math.js seems really frustrating to me.
^^ the first lines of a more complicated script look like this with
factory
To be more precise, I don't understand why don't we use ES6 modules more, instead of the
factory
function which to me seems worse in many ways (the code isn't DRY at all, worse IntelliSense, problems with circular dependency).I have a vague understanding that math.js has custom bundling support and some package-wide settings and that's the reason why
factory
was invented, but I think these should be doable with modules too.So what's the reason why we use
factory
again? 😁️The text was updated successfully, but these errors were encountered: