josdejong / typed-function

Runtime type-checking for JavaScript functions
MIT License
71 stars 19 forks source link

Proposal: provide mutating operations on typed functions #138

Closed gwhitney closed 2 years ago

gwhitney commented 2 years ago

I wanted to separate a thread that came up a bit in the conversation in #126: the idea of having mutating operations on typed functions to perform all of the operations needed in mathjs: adding signatures, possibly replacing the implementation for signatures, re-constructing the full runtime when new types/conversions are added. I would really like to make a strong pitch for this being a good opportunity for adding such operations (I am not proposing that we disable any existing operations). To that end, let me gather the benefits of such an approach. Some of these points are covered in the prior conversation, but some are new -- I thought it might be helpful to have them all in one place.

1) It would then be possible to change to a convention in mathjs that each exported typed function in mathjs is created once, and remains constant for the lifetime of the mathjs instance. Then there is never any problem with stale references, because every reference ever created is the true reference to the object.

2) Therefore, there is no longer a need to worry about when dependencies for a function are added compared to when the function itself is added. As long as all of the functions are initialized before they are used, all will be well.

3) This in turn facilitates an organizational style in which the code file for a function, say, sum, defines a createSum, which when called on a mathjs instance, adds sum to that instance. (It could import its prerequisites and call their create functions on the instance, or it could just use a utility that creates empty versions of its prerequisites on the instance if they are not there (e.g. 'ensure(math, 'add')' and then rely on the overall import sequence to ensure that 'add' really is implemented -- I am not yet certain which will produce a better overall code layout. But with constant mutable typed functions, either is workable.)

4) It seems to me that this gets us to where module loading is the tree-shaking/dependency tracking mechanism -- only the modules you import, directly or indirectly, would be included in a bundle. (As per https://github.com/josdejong/mathjs/issues/1975)

5) And then, there's no need at all to add all of the signatures for a function at once; they can be added in any order, and it wouldn't even matter if the signatures for 'complex' happened to be added before 'number'. This allows a shift to more type-centric code organization: your create function can just add relevant signatures to to the relevant functions, without jeopardizing any previous use.

5) There should then be no difference between extending a previously existing mathjs instance and building a new one from scratch. You just call all the desired create functions with that instance as an argument, in whatever order. You won't need to figure out whether you can just call math.import or if you need to write factories. (In short, factories would be gone.)

6) Further, since typed-functions are constant but mutable, they can all be made inherently lazy, so libraries using typed-function won't need to implement any laziness. Basically, creating a typed function initially just sets the implementation to one that generates the real implementation from the signatures and replaces itself with that real implementation. So creation is super fast, and the cost of ordering the signatures and checking for conversions etc. is only paid on the first call. Moreover, between creation time and the first call, any number of additional signatures can be added, and the ordering/etc will still only happen once, at first call.

7) And now typed-functions can much more easily adapt to adding conversions/types/signatures after their first call. When one of these things happens that would potentially invalidate the generated implementation, their implementation is just set back to the "generate the real implementation" one (like setting a "dirty" flag, except there is no flag -- it's just whatever the current implementation is). For conversions (as opposed to just adding a signature), this would involve a typed instance keeping a list of all of the typed functions it owns, so that when a conversion is added, it could rapidly go through and "reset" all of the typed functions that use the destination of that conversion, and then they would lazily re-generate themselves as they are called.

I really don't see how to do the bulk of these things with immutable typed-functions, especially not the lazy adaptation to new conversions. But even just accumulating functions on a mathjs instance: if math is a mathjs instance, and then math.add for example is a constant mutable typed-function object, then when the createSum function runs on that math object, you don't have to worry if the resulting math.sum function placed on that object somehow includes "hard" references to the math.add typed-function because they will never be invalidated. (In fact, in this scheme for efficiency it's perhaps best if hard references are what's compiled in, rather than lookups through the math object, but since single property accesses are presumably quite quick we probably don't really have to worry much about which it is that's getting compiled in.)

You recently write in #126 that you have a "strong preference" to "not append to existing typed-functions. Instead, I prefer to create new instances and take a pure, functional, immutable approach." But typed-functions are in the end just arrays of regular functions. So the questions of "purity" etc. should be about whether the functions in that array are pure or not, etc. Appending to that array is orthogonal to that question: it's a matter of bookkeeping as to how you keep track of the overall behavior of that operation. That is, mutability/immutability of typed-functions is to me entirely about whether references to them can go stale/be invalidated or not. From that point of view, it seems clear that there will be fewer headaches/less overall code complexity if it's impossible for a reference to a specific typed-function to ever go stale.

I will also say referring to the discussion in #126 that the question of constant, mutable typed-functions vs non-constant, immutable typed-functions is somewhat orthogonal to the question of "Option B" vs. "Option C". Either would work with B, but yes, C relies on constancy. Since we seem to be heading to B, I don't think that has any implications about the proposal here.

Again, I am not proposing that any operations be taken away from typed-function, just that mutating operations be added, so that it would be possible to build a prototype of a factory-free, module-import-based, accumulating-behaviors version of mathjs. If that experiment doesn't work, presumably the existence of the mutating functions won't do any harm to the existing implementation. Since it might involve a modest amount of breaking behavior (not certain til I go through generating a PR for it), this point at which you are contemplating a significant revamp anyway of how a typed-function implementation arranges to call other implementations seems like a very good opportunity to give it a try. I would be happy to make such a PR, I would just want to have the design for #126 more or less settled so I could base it on that, and would like to know whether you'd like to see the "built-in laziness" implemented as a part of it from the beginning.

In short, I think constant, mutating typed-function has a very strong potential to greatly simplify the organization of mathjs without losing tree-shaking or configurability of different instances of mathjs. Thanks for taking this proposal seriously, and I am looking forward to your feedback.

gwhitney commented 2 years ago

OK, I made a bunch of strong claims above and I thought that "showing" might be more convincing than "saying", so I built a tiny working prototype that has no factories and is based entirely on importing, but allows selective loading of operations and generic operations that depend only on other operations and will then automatically operate on any types those other operations are extended to. You can see it at https://code.studioinfinity.org/glen/picomath -- hope it is of interest.

josdejong commented 2 years ago

Thanks Glen, I've read your above reasoning and had a look at your picomath POC, I do love it. Now I get what you mean with "constant, mutating typed-functions", you totally convinced me that it can be a great approach 😄 . It's funny: the patten of the create functions are basically the same as the initial implementation of this reference function https://github.com/josdejong/typed-function/issues/126#issuecomment-1076390092 (which in the end we're implementing as referTo and referToSelf instead for good reasons). The pattern works out nicely in the context of picomath.

I have to give this a bit more thought: list the current pain-points, see which of the pain-points will be addressed with this approach (and what not), what it simplifies exactly, think through changes in perspective like moving from a function centric to a more data type centric approach. I also want to built a similar POC with this approach I had in mind, binding functions to a context and refer to dependencies via this.add etc. See how that compares. A lot of food for thought 😁 👍

gwhitney commented 2 years ago

moving from a function centric to a more data type centric approach

Just wanted to point out that operation-centric vs datatype-centric is actually orthogonal to the constant, mutating typed-function aspect. In other words, I could have organized the picomath POC in an operation centric way, in which there would be a file (or possibly directory) for each operation that adds the versions of that operation for each type it finds on the math object when it's executed, and we could arrange those operation-adding functions to be lazily re-executed when new types are added. (Let me know if you'd like to see the example re-done that way.) I just did the PoC with types primary because I was brought up to think in type-centric terms, and to show the flexibility that the mutable typed-function infrastructure allows.

josdejong commented 2 years ago

Ok I worked out a very minimal proof of concept too, for this idea I had to use this to refer to dependencies: https://github.com/josdejong/femtomath

In short: I do like the simple, flexibly API and absence of factory functions, but using this to refer to dependencies is quite a disaster, you easily loose the right context. Your concept of "constant functions", creating an instance only once, looks much more promising in that regard.

What both these PoC's do not (yet) address is: how do I know which dependencies a function has? What functions/modules do I have to load? I.e. if I would only load the subtract function, the dependencies it has (add, negate) would not be fulfilled.

gwhitney commented 2 years ago

how do I know which dependencies a function has? What functions/modules do I have to load?

Well in something like picomath, which has a well-defined structure of where the source for a given (type,operation) combination would be, it's easy enough to add a utility method picomath.importDependencies() that would find all functions with no implementations and import them for all types that are currently defined in the instance (and iterate until the process stabilizes). But that will of course use dynamic import. Does that sound like something that would fill the need you see? If you would like to see this utility in action let me know and I will add it to picomath along with a test that imports and adds just the number and complex types (no operations) and the generic 'subtract' operation and then calls picomath.importDependencies() and produces a working instance where you can subtract any combination of numbers and/or complex numbers.

If that doesn't seem to be on point for what you are looking for, let me know in more detail what capability you would like to have and I will think about whether there is a suitable way to add it to picomath.

josdejong commented 2 years ago

It's indeed possible to write a script to return the dependencies for you. Right now in mathjs there is such a script, generating files like src/entry/dependenciesAny/dependenciesAdd.generated.js. So it is solvable like that, but it is relatively complicated/fragile so if it wouldn't be needed in the first place that would be great of course.

Dynamic imports will indeed be a challenge: that doesn't play nice with bundling and tree shaking. I think though that it may not be necessary when having this script in place that generates "entry" files with the dependencies listed per function or something like that.

Open topics that come to mind when thinking about a new architecture for mathjs, and looking at the picomath PoC:

  1. Figure out resolving dependencies, bundling, tree-shaking
  2. Figure out the best organization for the functions (per operation, per data type, or a mix between this)
  3. Figure out how to make typed-function and mathjs play nice with TypeScript. Do we still want/need typed-function? Is there an alternative that can generate something similar based on TypeScript definitions?
  4. Figure out the exact API. I do like the concept of picomath a lot, with the constant, mutating typed-functions. We can iterate on that I think, similar to how we iterated on the typed.referTo and typed.referToSelf functions.
  5. Figure out how we can get the concept of zero-based indexes for JS functions, one-based indexes for expressions in the new architecture.
  6. Figure out how the chained API can be re-implemented with the new architecture (there is currently a listener implemented that triggers when a new function is imported for example)
  7. Figure out how to set up configuration, how to change and consume it
  8. Figure out if we still need a listener structure that is currently in place for imports and config changes.
  9. Work out a detailed experiment with all of the above to see if it works well out in practice.
josdejong commented 2 years ago

As for the original topic of providing mutating operations: I think we will need them in the future (you convinced me 😉). But the future is not ironed out yet, and I don't think it will be within say "days or weeks", so I'm not sure whether it is a good idea to implement something in this regard already and ship it with v3.

I think it will boil down to a new method like addSignature (or maybe even let typed functions react on the fly when new data types/conversions are added to the typed instance???).

gwhitney commented 2 years ago

Well, here's a specific proposal that would be quick to implement that we could put in now so that we have it in our pocket for serious experimentation in mathjs, without disrupting the current method of operation or necessitating any significant refactor in mathjs:

Barring this specific proposal or some mild variant of it that you might propose, I'd agree that it should just be postponed for the indefinite future. But it might be nice to have something like this to play around with as long as we are at it.

josdejong commented 2 years ago

I think those proposals make sense, thanks. I expect this function merge is quite easy to implement when #144 is finished 😁 . Both points look like non-breaking changes, so there may be no pressure to get it finished on short term for v3.

gwhitney commented 2 years ago

Here is the only subtle point, which is why I was keen to try it in the midst of a breaking change, rather than later: you cannot change the function body of constant function object, even if it otherwise mutates. So to implement anything like merge or any other behavior changing mutation, the original function body of any typed function has to be some sort of immediate dispatch to (an)other function(s) that it finds by examining its mutable properties. (See the pseudo typed functions of picomath for an example of this; all individual typed functions have exactly the same actual function body.) Since that's a nontrivial refactor of how an individual typed function works from where it is now, I have a niggling worry that switching to this immediate dispatch will involve some minor degree of breaking change (and also that the performance of such a scheme will definitely need to be checked). If you prefer not to wait on trying this out, I understand; or I can give it a whirl. Just let me know. And this would definitely come after #144.

gwhitney commented 2 years ago

And now to reply to your "big comment":

a script to return the dependencies for you Right, rather than using dynamic import it's even easier to make a utility that writes a script which will import everything statically for you to satisfy a given function or set of functions.

But really the way I was envisioning this working in practice is that there would be some number of existing imports that statically import other clusters etc to give you the main selections you need. Like obviously a main module that just imports everything, like the current mathjs; and an analogue of mathjs/number that only imports the number type and just the number versions of all the operations; and maybe addon modules like mathjs/unit that you could also load if you just wanted numbers and units; and all of those would just work by static imports chasing through the source tree, so it seems like they would work well with webpack or similar tree-shaking approaches. The kinds of utilities that either do dynamic import or write scripts would be just for creating specialized instances that have a selection of functions and/or types that cuts across the ones we have provided "standard" modules for. In particular, I wouldn't see any need for something like src/entry/dependenciesAny.generated.js in the standard package, the import tree should just make it work.

So on your nine-point topic list:

  1. Figure out resolving dependencies, bundling, tree-shaking

The above is pretty much the thoughts I have on that; maybe unsophisticated, but I am not clear on what is the key example of a tricky task in this area that needs to be solved? Typically you should just be able to import the main module, and get what you need by the recursive imports encountered, and I assume webpack and friends deal well with that.

  1. Figure out the best organization for the functions (per operation, per data type, or a mix between this)

Agreed. This is a matter of taste I think to a large extent, and I am fine with any organization that supports programmatically knowing the path to import for a specific type/operation combination.

  1. Figure out how to make typed-function and mathjs play nice with TypeScript. Do we still want/need typed-function? Is there an alternative that can generate something similar based on TypeScript definitions?

Personally I see this as orthogonal and/or a follow-on to a better organization for mathjs that doesn't require factories and has less repeated information. Priority for this item comes if there seems to be advantage to either (a) better support for TypeScript clients than simply a couple of hand-maintained declaration files, or (b) use of TypeScript in the mathjs implementation for whatever benefits that might provide. Happy to help out, but I am not a huge fan of TypeScript even though I am a fan of static typing in general, so I'm not going to be providing much drive here.

  1. Figure out the exact API. I do like the concept of picomath a lot, with the constant, mutating typed-functions. We can iterate on that I think, similar to how we iterated on the typed.referTo and typed.referToSelf functions.

Roger that.

  1. Figure out how we can get the concept of zero-based indexes for JS functions, one-based indexes for expressions in the new architecture.

Right, I haven't thought at all about whether/how this approach offers any better option than the transforms (which could of course just be implemented as is, at least as first, on top of a constant-mutating style of typed function).

  1. Figure out how the chained API can be re-implemented with the new architecture (there is currently a listener implemented that triggers when a new function is imported for example)

I think this is easy with the picomath style and one of its real wins. That's why I implemented the example of functions that lazily refresh themselves when the config object on a picomath instance changes (it's in a branch in the picomath repository). I think exactly the same principle will work for lazily creating chain functions.

  1. Figure out how to set up configuration, how to change and consume it

Ditto on my last comment.

  1. Figure out if we still need a listener structure that is currently in place for imports and config changes.

Ditto again.

  1. Work out a detailed experiment with all of the above to see if it works well out in practice.

Yup that's the main reason I was suggesting putting in at least the basic mutating method now in my merge proposal above, so that it's possible to make a more realistic prototype.

josdejong commented 2 years ago

Here is the only subtle point, which is why I was keen to try it in the midst of a breaking change, rather than later: you cannot change the function body of constant function object, even if it otherwise mutates.

Hm that is true. So that is a showstopper for the quick implementation of a .merge() function, right? (I guess we could come up with a workaround, but I prefer doing it right and not hacky).

Unless you still see an easy/neat/simple way to implement a .merge() on short term, I prefer postponing changing typed-function to mutating operations for the next breaking release and get the current one (v3) finished first. I would love to take the time to iron out these new ideas, get the bigger picture more clear, take time to work out a full fledged experiment, etc. That will take time. Does that make sense?

But really the way I was envisioning this working in practice is that there would be some number of existing imports that statically import other clusters etc to give you the main selections you need.

yes that makes sense. So instead of facilitating index files per function containing all dependencies of a single file, you would have a index files per category or module (like arithmetic, trigonometric, algebra etc)? We can indeed make these index files by hand and put a unit test on top to make sure all dependencies are satisfied. Then there would be no "smart" auto generation of index files needed.

(1) Yes solved with your above comments I think. Only, dynamic imports would complicate things. If we don't use that it should be straightforward.

(2) Yeah, my rough idea right now is: one module per data type, containing all "basic" functions for this data type, typically the arithmetic functions. And modules per mathematical category, that typically are generic can handle any numeric type.

(6) That would be great if it's that straightforward!

(9) I think in picomath you can keep the (constant) proxy function as it is, and under the hood let it use a typed-function. This typed-function can be updated by doing typedFn = typed(typedFn, newSignatures), right?

gwhitney commented 2 years ago

So that is a showstopper for the quick implementation of a .merge() function, right? (I guess we could come up with a workaround, but I prefer doing it right and not hacky).

Unless you still see an easy/neat/simple way to implement a .merge() on short term

Well, the only way I see to implement merge is to have a standard main function body that all typed functions have. It could either just be:

function tf () {
  return tf._do(arguments)
}

and then the typed function can manipulate the _do property as it likes, or it could basically be the generic dispatch, but it still needs to call something for the specialized quick dispatch first. My main worry, though, is that the indirection of the execution will kill some kind of optimizations that the JavaScript implementations do, and make typed-functions too slow. picomath was too tiny to detect this sort of thing. So benchmarking with the real thing and mathjs is critical.

If you feel this is too much to take on now, I understand.

gwhitney commented 2 years ago

Oh, another compromise would be that the fixed function body is the quick-dispatch for whatever signatures are first added, which presumably we'd arrange to be the 'number' ones, and if none of them hits it goes to generic dispatch, which would use mutable arrays of tests and implementations.

A couple of things on the other numbered issues: Your basic idea for (2) seems pretty reasonable. And on (9) I agree I could make a picomath where the pseudo-typed function is based on a current typed-function under the hood; but what we really need to test is mathjs on top of this, so it's much easier to just make a typed-function that has a merge() in it -- the only worry is if allowing for the merge by having a fixed function body slowed mathjs down even if it never uses the merge.

josdejong commented 2 years ago

Oh, another compromise would be that the fixed function body is the quick-dispatch for whatever signatures are first added

Yes indeed, that is what I meant with my comment (9) https://github.com/josdejong/typed-function/issues/138#issuecomment-1092149460 . So picomath could just put a fixed wrapper/proxy function around the typed-functions, and internally replace it's typed-function implementation when new signatures are added via typedFn = typed(typedFn, newSignatures).

Putting this "standard main function body" around every typed-function feels hacky, and indeed may influence performance etc. Since we can implement in the picomath approach, I think there is no need to adjust typed-function itself right now. It would only move the implementation of the "standard main function body" from picomath to typed-function, but would not really change anything. So I prefer leaving typed-function as it is, and first explore the picomath approach further, adding TypeScript to the mix, etc, and once settled, see how we need to change typed-function to accommodate for the new approach. Does that make sense?

gwhitney commented 2 years ago

Well, I guess leave this for now then, and when the dust settles on v3 one way or another (see the timing results in #144), I can just make a typed-function identical to where we end up except it has a fixed function body that fowards to the function that's generated now, and run the benchmarks on mathjs with that typed-function (which will behave identically to the non-forwarding one). Do you think the currently existing benchmarks on mathjs are sufficient to detect any problems that the forwarding might cause?

Anyhow, the results of an appropriate benchmark for mathjs/typed-function with and without such a forwarding step from a fixed function body should make it clear whether or not there is any future for mutable typed functions. So I will put that high on my list for after typed-function v3 is released (if it will be in roughly its current form) and mathjs v11 is released.

gwhitney commented 2 years ago

OK, so I tried a version of typed-function which changes the function body of every typed function to be:

      function fixed () {
        return fixed.the_typed_fn.apply(this, arguments);
      }

(so that we could then change the_typed_fn property to allow for mutating the behavior of the typed function). Here's the before-and after benchmarks:

typed-function> node benchmark/benchmark.js  # v3 as published
execute: vanillaAdd                      x 31,103,190 ops/sec ±0.49% (95 runs sampled)
execute: typedAdd                        x 28,458,235 ops/sec ±0.37% (95 runs sampled)
execute:  1 signature,   0 conversions   x 29,648,517 ops/sec ±0.18% (95 runs sampled)
execute: 10 signatures,  0 conversions   x 28,971,766 ops/sec ±0.09% (93 runs sampled)
execute:  1 signatures, 10 conversions   x 10,726,544 ops/sec ±0.51% (94 runs sampled)
execute: 10 signatures, 10 conversions   x 4,290,538 ops/sec ±0.19% (97 runs sampled)
execute:  1 signature,  20 params        x 956,374 ops/sec ±0.18% (98 runs sampled)
create:   1 signature,   0 conversions   x 231,400 ops/sec ±10.11% (96 runs sampled)
create:  10 signatures,  0 conversions   x 27,491 ops/sec ±0.08% (96 runs sampled)
create:   1 signatures, 10 conversions   x 65,011 ops/sec ±0.25% (90 runs sampled)
create:  10 signatures, 10 conversions   x 5,414 ops/sec ±0.13% (99 runs sampled)
create:   1 signature,  20 params        x 46,371 ops/sec ±0.25% (95 runs sampled)
First typed universe created 1 functions
typed2 universe created 1993631 functions

typed-function> node benchmark/benchmark.js
execute: vanillaAdd                      x 30,455,786 ops/sec ±0.61% (96 runs sampled)
execute: typedAdd                        x 10,871,852 ops/sec ±0.54% (93 runs sampled)
execute:  1 signature,   0 conversions   x 13,266,428 ops/sec ±1.06% (93 runs sampled)
execute: 10 signatures,  0 conversions   x 13,393,093 ops/sec ±0.52% (97 runs sampled)
execute:  1 signatures, 10 conversions   x 6,589,936 ops/sec ±0.38% (95 runs sampled)
execute: 10 signatures, 10 conversions   x 3,331,311 ops/sec ±0.58% (96 runs sampled)
execute:  1 signature,  20 params        x 894,698 ops/sec ±1.00% (97 runs sampled)
create:   1 signature,   0 conversions   x 203,161 ops/sec ±1.75% (92 runs sampled)
create:  10 signatures,  0 conversions   x 26,473 ops/sec ±1.09% (92 runs sampled)
create:   1 signatures, 10 conversions   x 58,583 ops/sec ±1.40% (92 runs sampled)
create:  10 signatures, 10 conversions   x 5,206 ops/sec ±1.20% (94 runs sampled)
create:   1 signature,  20 params        x 43,982 ops/sec ±0.60% (93 runs sampled)
First typed universe created 1 functions
typed2 universe created 1745788 functions

I am guessing that this represents an unacceptable performance hit, and so the idea of mutating typed functions is dead unless/until someone comes up with a different mechanism for allowing a typed function to mutate that performs better. If that's the case, feel free to close this issue (I'll reopen if I have any brainstorms about how to do this faster).

josdejong commented 2 years ago

Wow I hadn't expected such a big difference🤔.

So at least it's not a good idea from a performance point of view to introduce such a wrapper function in mathjs around typed-function. So this idea for a quick workaround will not work out unfortunately.

I suppose when working out the picomath approach (instead of a wrapper on top of typed-function), this performance hit would not pop up though.

gwhitney commented 2 years ago

Just reproduced the above experiment with basically the same results, just slightly less bad looking, but still I don't think acceptable. Am pasting the results below for completeness' sake. But then I have a slightly different method I will try to see if it's any better.

typed-function> git checkout forward_exp_1
Switched to branch 'forward_exp_1'
typed-function> node benchmark/benchmark.js
execute: vanillaAdd                      x 31,854,972 ops/sec ±0.65% (91 runs sampled)
execute: typedAdd                        x 11,381,249 ops/sec ±0.53% (91 runs sampled)
execute:  1 signature,   0 conversions   x 13,668,752 ops/sec ±0.34% (84 runs sampled)
execute: 10 signatures,  0 conversions   x 14,324,957 ops/sec ±0.30% (95 runs sampled)
execute:  1 signatures, 10 conversions   x 6,916,985 ops/sec ±0.49% (95 runs sampled)
execute: 10 signatures, 10 conversions   x 3,362,985 ops/sec ±0.32% (91 runs sampled)
execute:  1 signature,  20 params        x 989,801 ops/sec ±0.54% (80 runs sampled)
create:   1 signature,   0 conversions   x 221,649 ops/sec ±0.45% (91 runs sampled)
create:  10 signatures,  0 conversions   x 28,460 ops/sec ±0.21% (97 runs sampled)
create:   1 signatures, 10 conversions   x 62,480 ops/sec ±0.33% (99 runs sampled)
create:  10 signatures, 10 conversions   x 5,403 ops/sec ±0.18% (97 runs sampled)
create:   1 signature,  20 params        x 46,496 ops/sec ±0.40% (90 runs sampled)
First typed universe created 1 functions
typed2 universe created 1918399 functions
typed-function> git checkout develop
Switched to branch 'develop'
Your branch is up to date with 'origin/develop'.
typed-function> node benchmark/benchmark.js
execute: vanillaAdd                      x 32,383,146 ops/sec ±0.23% (95 runs sampled)
execute: typedAdd                        x 29,145,380 ops/sec ±0.30% (90 runs sampled)
execute:  1 signature,   0 conversions   x 31,218,249 ops/sec ±0.26% (98 runs sampled)
execute: 10 signatures,  0 conversions   x 30,604,528 ops/sec ±1.10% (92 runs sampled)
execute:  1 signatures, 10 conversions   x 10,789,753 ops/sec ±0.48% (96 runs sampled)
execute: 10 signatures, 10 conversions   x 4,526,721 ops/sec ±0.63% (91 runs sampled)
execute:  1 signature,  20 params        x 1,042,205 ops/sec ±0.36% (97 runs sampled)
create:   1 signature,   0 conversions   x 227,024 ops/sec ±0.27% (93 runs sampled)
create:  10 signatures,  0 conversions   x 26,617 ops/sec ±0.12% (96 runs sampled)
create:   1 signatures, 10 conversions   x 63,346 ops/sec ±0.19% (94 runs sampled)
create:  10 signatures, 10 conversions   x 5,377 ops/sec ±0.24% (95 runs sampled)
create:   1 signature,  20 params        x 47,317 ops/sec ±0.47% (85 runs sampled)
First typed universe created 1 functions
typed2 universe created 1911952 functions
gwhitney commented 2 years ago

OK, here's my second attempt:

typed-function> node benchmark/benchmark.js
execute: vanillaAdd                      x 32,427,770 ops/sec ±0.38% (88 runs sampled)
execute: typedAdd                        x 7,621,374 ops/sec ±0.35% (94 runs sampled)
execute:  1 signature,   0 conversions   x 12,491,247 ops/sec ±0.50% (96 runs sampled)
execute: 10 signatures,  0 conversions   x 12,065,914 ops/sec ±0.37% (90 runs sampled)
execute:  1 signatures, 10 conversions   x 6,106,441 ops/sec ±1.54% (87 runs sampled)
execute: 10 signatures, 10 conversions   x 2,244,002 ops/sec ±0.13% (99 runs sampled)
execute:  1 signature,  20 params        x 1,126,142 ops/sec ±1.24% (95 runs sampled)
create:   1 signature,   0 conversions   x 179,911 ops/sec ±1.08% (92 runs sampled)
create:  10 signatures,  0 conversions   x 28,200 ops/sec ±0.21% (93 runs sampled)
create:   1 signatures, 10 conversions   x 61,929 ops/sec ±0.24% (96 runs sampled)
create:  10 signatures, 10 conversions   x 5,466 ops/sec ±0.22% (97 runs sampled)
create:   1 signature,  20 params        x 46,069 ops/sec ±0.32% (93 runs sampled)
First typed universe created 1 functions
typed2 universe created 1710831 functions
gwhitney commented 2 years ago

OK, this is no better than the first attempt (the idea in the second attempt was rather than immediately forwarding to a function that can be changed, make all of those constants and functions like ok0 and test01 and fn0 be properties of the typed function, so that they can be changed, but leave the skeleton of the basic implementation of the typed function the same. I guess as soon as you call a potentially changeable function object, you're just in for slowness. So I'm back in the state of presuming that mutating typed functions are a dead letter unless someone comes up with a better implementation idea.

gwhitney commented 2 years ago

Fine with me if you want to close this issue...

josdejong commented 2 years ago

Thanks for the latest benchmarks. Too bad that it isn't "just" working.

I think it may work to try work out a fresh approach starting from picomath instead of trying to glue typed-function on it as-is. It will probably be a lot of trial and error though.

Yes let's close this issue, I think we've explored this idea far enough, thanks!