API to specify a function name and multiple signatures in a single file #12

Closed
opened 2025-04-09 16:01:03 +00:00 by josdejong · 19 comments
Collaborator

I've been reading https://code.studioinfinity.org/StudioInfinity/nanomath/wiki/Item-Specifications, and now I'm thinking about how to specify multiple signatures of a single function in a single file in a neat way.

Maybe it is interesting to do something like this, putting the function name as a factory function argument:

const add = onType('add', [Complex, Complex], (math, [T, U]) => { ... })

And then, to implement multiple signatures:

const add = [
  onType('add', [number, number], (math) => { ... }),
  onType('add', [Complex, Complex], (math, [T, U]) => { ... }),
  // ...
]

Or maybe rename onType into create or factory, for example:

const add = [
  create('add', [number, number], (math) => { ... }),
  create('add', [Complex, Complex], (math, [T, U]) => { ... }),
  // ...
]

Thinking about it, it could also just be wrapping signatures in an array:

const add = [
  onType([number, number], (math) => { ... }),
  onType([Complex, Complex], (math, [T, U]) => { ... }),
  // ...
]

What do you think?

I've been reading https://code.studioinfinity.org/StudioInfinity/nanomath/wiki/Item-Specifications, and now I'm thinking about how to specify multiple signatures of a single function in a single file in a neat way. Maybe it is interesting to do something like this, putting the function name as a factory function argument: ```js const add = onType('add', [Complex, Complex], (math, [T, U]) => { ... }) ``` And then, to implement multiple signatures: ```js const add = [ onType('add', [number, number], (math) => { ... }), onType('add', [Complex, Complex], (math, [T, U]) => { ... }), // ... ] ``` Or maybe rename `onType` into `create` or `factory`, for example: ```js const add = [ create('add', [number, number], (math) => { ... }), create('add', [Complex, Complex], (math, [T, U]) => { ... }), // ... ] ``` Thinking about it, it could also just be wrapping signatures in an array: ```js const add = [ onType([number, number], (math) => { ... }), onType([Complex, Complex], (math, [T, U]) => { ... }), // ... ] ``` What do you think?
Owner

Well currently I just allow

const add = onType(
   [Number, Number], (math) => { ... },
   [Complex, Complex], (math, [T, U]) => { ... }
)

(see src/core/__test__/TestDispatcher.spec.js at line 20, for example). In other words, you can just alternate type patterns and corresponding values, for as many or few as you like (often just one pair). This convention seemed to me to be the simplest analogue of the object key - value interface in mathjs 14. But do you like it better how it looks if you repeat onType for each type pattern?

Or if you like a list of pairs better, we could switch to

const add = onType(
   [[Number, Number], (math) => { ... }],
   [[Complex, Complex], (math, [T, U]) => { ... }],
)

or something like that.

Another alternative, if you don't mind repeating a function symbol, would be:

export const add = {
   [pattern(Number, Number)]:  (math) => { ... },
   [pattern(Complex, Complex)]: (math, [T, U]) => { ... },
}

Here we avoid parsing by having our pattern function set up to return a string which more or less acts like a hash -- it puts the actual TypePattern object into some global repository, keyed by that string. Then when this add definition gets merged into the TypeDispatcher, it looks up the keys of the object associated with add in that global repository to find the TypePatterns it should use. Don't get me wrong, the string returned by pattern could be human-readable; it just has to be different for each different TypePattern.

But the notation I currently implemented is the most concise one I could think of.

Let me know which of these conventions you prefer and if it's different than the current one, I will make a PR to switch this prototype over. Same goes if you come up with some "sleeker" for return-type annotations.

Well currently I just allow ``` const add = onType( [Number, Number], (math) => { ... }, [Complex, Complex], (math, [T, U]) => { ... } ) ``` (see `src/core/__test__/TestDispatcher.spec.js` at line 20, for example). In other words, you can just alternate type patterns and corresponding values, for as many or few as you like (often just one pair). This convention seemed to me to be the simplest analogue of the object key - value interface in mathjs 14. But do you like it better how it looks if you repeat `onType` for each type pattern? Or if you like a list of pairs better, we could switch to ``` const add = onType( [[Number, Number], (math) => { ... }], [[Complex, Complex], (math, [T, U]) => { ... }], ) ``` or something like that. Another alternative, if you don't mind repeating a function symbol, would be: ``` export const add = { [pattern(Number, Number)]: (math) => { ... }, [pattern(Complex, Complex)]: (math, [T, U]) => { ... }, } ``` Here we avoid parsing by having our `pattern` function set up to return a string which more or less acts like a hash -- it puts the actual TypePattern object into some global repository, keyed by that string. Then when this `add` definition gets merged into the TypeDispatcher, it looks up the keys of the object associated with `add` in that global repository to find the TypePatterns it should use. Don't get me wrong, the string returned by `pattern` could be human-readable; it just has to be different for each different TypePattern. But the notation I currently implemented is the most concise one I could think of. Let me know which of these conventions you prefer and if it's different than the current one, I will make a PR to switch this prototype over. Same goes if you come up with some "sleeker" for return-type annotations.
Author
Collaborator

Ah I see. The alternating pairs are indeed concise, but I think it would be better to have a bit more explicit API. When using automatic formatters for example, I expect all arguments to be put on separate lines and there is no way to explain a formater that it should maintain pairs of two in this specific case.

I think we should go for an API that is simplest to explain.

First, I'm still thinking about keeping onType anonymous vs putting the function name inside it like onType("add", ...). I think it's best to keep them anonymous, that is probably most flexible but I'm not sure. Any thoughts?

Then, I like your idea of a list of pairs, I think that is better then alternating pairs. Still, I think my earlier example of an array containing multiple onType(...) calls is easier to read and explain: every onType contains just a single signature, it's very explicit and I think easier to read than adding more (and nested) brackets.

const add = [
  onType([number, number], (math) => { ... }),
  onType([Complex, Complex], (math, [T, U]) => { ... }),
  // ...
]

Would that be ok with you?

Ah I see. The alternating pairs are indeed concise, but I think it would be better to have a bit more explicit API. When using automatic formatters for example, I expect all arguments to be put on separate lines and there is no way to explain a formater that it should maintain pairs of two in this specific case. I think we should go for an API that is simplest to explain. First, I'm still thinking about keeping `onType` anonymous vs putting the function name inside it like `onType("add", ...)`. I think it's best to keep them anonymous, that is probably most flexible but I'm not sure. Any thoughts? Then, I like your idea of a list of pairs, I think that is better then alternating pairs. Still, I think my earlier example of an array containing multiple `onType(...)` calls is easier to read and explain: every `onType` contains just a single signature, it's very explicit and I think easier to read than adding more (and nested) brackets. ```js const add = [ onType([number, number], (math) => { ... }), onType([Complex, Complex], (math, [T, U]) => { ... }), // ... ] ``` Would that be ok with you?
Owner

@josdejong wrote in #12 (comment):

When using automatic formatters for example, I expect all arguments to be put on separate lines and there is no way to explain a formater that it should maintain pairs of two in this specific case.

Yes this is exactly why I hate the recent movement to enforced code formatting. When done well, code layout can and should carry information that makes code easier to read, understand, and maintain. prettier and the like wash that all away, in the name of avoiding bikeshedding, which is insufficient reason to me to throw away the advantages of good layout.

So I will never use a formatter in my own project. But this is a collaborative project, so I will respond on the substance next.

@josdejong wrote in https://code.studioinfinity.org/StudioInfinity/nanomath/issues/12#issuecomment-2701: > When using automatic formatters for example, I expect all arguments to be put on separate lines and there is no way to explain a formater that it should maintain pairs of two in this specific case. Yes this is exactly why I hate the recent movement to enforced code formatting. When done well, code layout can and should carry information that makes code easier to read, understand, and maintain. `prettier` and the like wash that all away, in the name of avoiding bikeshedding, which is insufficient reason to me to throw away the advantages of good layout. So I will never use a formatter in my own project. But this is a collaborative project, so I will respond on the substance next.
Owner

Of course we can adopt a different syntax if it will be more comfortable. Since we will be repeating the onType functionality so many times, could we use something really short, like on(NumberT, Optional(BooleanT)) or match(NumberT, Optional(BooleanT))?

Then we can either do

export const add = [
   on(BooleanT, ....),
   on(NumberT, ...)
]

Or

export const add = {
   [on(BooleanT)]: ....,
   [on(NumberT)]: ....
}

Whichever you prefer -- let me know. And either way is it OK if we also allow

const factorial = on(NumberT, ...)

in the case when there is only one signature being defined in this location?

Let me know your preferences in all of these things, and I will make PR switching over.

Of course we can adopt a different syntax if it will be more comfortable. Since we will be repeating the `onType` functionality so many times, could we use something really short, like `on(NumberT, Optional(BooleanT))` or `match(NumberT, Optional(BooleanT))`? Then we can either do ``` export const add = [ on(BooleanT, ....), on(NumberT, ...) ] ``` Or ``` export const add = { [on(BooleanT)]: ...., [on(NumberT)]: .... } ``` Whichever you prefer -- let me know. And either way is it OK if we also allow ``` const factorial = on(NumberT, ...) ``` in the case when there is only one signature being defined in this location? Let me know your preferences in all of these things, and I will make PR switching over.
Author
Collaborator

😂 I hear some frustration about formatters. I myself love them, it helps a lot ensuring a consistent and unified style in a code base, and it saves me time when styling and indentation is automatically applied when saving a file. But yes, sometimes it's a pity that you can't format some specific code the way you want. Of course, you can configure a formatter with many or just a few rules, only enforcing what you find important. Anyway, this is a bit off topic.

I like the name on, it is very short and it reads quite (I hope people will not confuse it with an event handler like on('click', ...)).

My preference is to allow both an array with on(...) and a single on(...):

export const add = [
   on([Boolean, Boolean], (math) => { ... }),
   on([Number, Number], (math) => { ... }),
   on([Complex, Complex], (math, [T, U]) => { ... }),
   // ...
]

const factorial = on([Number], ...)

I guess you still want to keep the first argument of on an Array with the types of the arguments of the function, right?

😂 I hear some frustration about formatters. I myself love them, it helps a lot ensuring a consistent and unified style in a code base, and it saves me time when styling and indentation is automatically applied when saving a file. But yes, sometimes it's a pity that you can't format some specific code the way you want. Of course, you can configure a formatter with many or just a few rules, only enforcing what you find important. Anyway, this is a bit off topic. I like the name `on`, it is very short and it reads quite (I hope people will not confuse it with an event handler like `on('click', ...)`). My preference is to allow both an array with `on(...)` and a single `on(...)`: ```js export const add = [ on([Boolean, Boolean], (math) => { ... }), on([Number, Number], (math) => { ... }), on([Complex, Complex], (math, [T, U]) => { ... }), // ... ] const factorial = on([Number], ...) ``` I guess you still want to keep the first argument of `on` an Array with the types of the arguments of the function, right?
Owner

@josdejong wrote in #12 (comment):

Of course, you can configure a formatter with many or just a few rules, only enforcing what you find important.

I actually do like a linter that complains about some egregious things, without changing anything.

Anyway, this is a bit off topic.

Agreed.

@josdejong wrote in https://code.studioinfinity.org/StudioInfinity/nanomath/issues/12#issuecomment-2704: > Of course, you can configure a formatter with many or just a few rules, only enforcing what you find important. I actually do like a _linter_ that complains about some egregious things, without changing anything. > Anyway, this is a bit off topic. Agreed.
Owner

@josdejong wrote in #12 (comment):

I hope people will not confuse it with an event handler like on('click', ...)

Yes, that's why I proposed match -- reminiscent in syntax and semantics of other languages where the action to take can be conditioned on a sort of type-matching expression, and the arguments to this function actually are patterns (like Optional(BooleanT) rather than types, it's just that types are automatically converted into patterns that match themselves. So if you prefer match, I'm totally fine with that. Another option would be with. Just let me know.

My preference is to allow both an array with on(...) and a single on(...)

Got it.

I guess you still want to keep the first argument of on an Array with the types of the arguments of the function, right?

Well, I think it is unambiguous and convenient in the very common case of a unary function to allow just the "bare" type -- so I really did mean factorial = on(Number, n => {...}). Any objection?

@josdejong wrote in https://code.studioinfinity.org/StudioInfinity/nanomath/issues/12#issuecomment-2704: > I hope people will not confuse it with an event handler like `on('click', ...)` Yes, that's why I proposed `match` -- reminiscent in syntax and semantics of other languages where the action to take can be conditioned on a sort of type-matching expression, and the arguments to this function actually are patterns (like `Optional(BooleanT)` rather than types, it's just that types are automatically converted into patterns that match themselves. So if you prefer `match`, I'm totally fine with that. Another option would be `with`. Just let me know. > My preference is to allow both an array with on(...) and a single on(...) Got it. > I guess you still want to keep the first argument of on an Array with the types of the arguments of the function, right? Well, I think it is unambiguous and convenient in the very common case of a unary function to allow just the "bare" type -- so I really did mean `factorial = on(Number, n => {...})`. Any objection?
Author
Collaborator

Yeah, good point, I think match is better than on. De dispatcher is indeed "just" doing pattern matching :). Ok let's go for match then.

Well, I think it is unambiguous and convenient in the very common case of a unary function to allow just the "bare" type -- so I really did mean factorial = on(Number, n => {...}). Any objection?

It is nice and concise indeed. I think though that it may be harder to describe: a variable number of arguments followed by one more required argument. For example I'm not sure how to describe that in TypeScript. Most programming languages allow variable arguments only as last argument. So, unless there is a neat solution for describing the types, I think it is safer to stick with a simpler notation match(argumentTypes: Array, factory: Function) than a non-standard vararg followed by a required arg.

Yeah, good point, I think `match` is better than `on`. De dispatcher is indeed "just" doing pattern matching :). Ok let's go for `match` then. > Well, I think it is unambiguous and convenient in the very common case of a unary function to allow just the "bare" type -- so I really did mean `factorial = on(Number, n => {...})`. Any objection? It is nice and concise indeed. I think though that it may be harder to describe: a variable number of arguments followed by one more required argument. For example I'm not sure how to describe that in TypeScript. Most programming languages allow variable arguments only as last argument. So, unless there is a neat solution for describing the types, I think it is safer to stick with a simpler notation `match(argumentTypes: Array, factory: Function)` than a non-standard vararg followed by a required arg.
Owner

No sorry I was not being clear:

export const add = match([NumberT, NumberT], (a, b) => a + b)
export const log = match(NumberT, x => Math.log(x))

I am not proposing any sort of variable numbers of arguments to match. Just that the first argument can either be an array (for zero or multiple arguments) or a single type, for convenience for unary signatures. It would still be supported to write log = match([NumberT], ...) but since we have a lot of unary methods, it will save a lot of brackets to make them optional. Is that OK?

No sorry I was not being clear: ``` export const add = match([NumberT, NumberT], (a, b) => a + b) export const log = match(NumberT, x => Math.log(x)) ``` I am not proposing any sort of variable numbers of arguments to match. Just that the first argument can either be an array (for zero or multiple arguments) or a single type, for convenience for unary signatures. It would still be supported to write `log = match([NumberT], ...)` but since we have a lot of unary methods, it will save a lot of brackets to make them optional. Is that OK?
Author
Collaborator

Ah, ok, yeah that sounds good to me. It is quite a common case indeed so nice to have.

Ah, ok, yeah that sounds good to me. It is quite a common case indeed so nice to have.
glen added the
priority
maintenance
labels 2025-04-22 01:51:24 +00:00
Owner

OK, I am working on this now, and just wanted to mention that at the moment, it works to simply export a "bare" factory:

export const unequal = (math, types) => {
   const eq = math.equal.resolve(types)
   return ReturnsAs(eq, (...args) => !eq(...args))
}

This means: use the associated factory for unequal no matter what the actual arguments and their types are. It is precisely a shorthand for

export const unequal = match(Passthru, (math, types) => {
   const eq = math.equal.resolve(types)
   return ReturnsAs(eq, (...args) => !eq(...args))
})

(where the difference between Passthru and Any is that Any matches a single argument (type) whereas Passthru matches any argument list of any length, and the difference between Passthru and Multiple(Any) is that Multiple(Any) collects its argument (types) into a single Array, whereas Passthru delivers them as their original list, not collected).

Would you prefer to require the match(Passthru, ...) for uniformity, or are you OK with allowing this shorthand for when a factory really can handle any argument list? It comes up mainly in generic functions that just "translate" calls to a method into some call or calls to other method(s), such as unequal here being translated into the negation of equal.

OK, I am working on this now, and just wanted to mention that at the moment, it works to simply export a "bare" factory: ``` export const unequal = (math, types) => { const eq = math.equal.resolve(types) return ReturnsAs(eq, (...args) => !eq(...args)) } ``` This means: use the associated factory for `unequal` no matter what the actual arguments and their types are. It is precisely a shorthand for ``` export const unequal = match(Passthru, (math, types) => { const eq = math.equal.resolve(types) return ReturnsAs(eq, (...args) => !eq(...args)) }) ``` (where the difference between `Passthru` and `Any` is that `Any` matches a single argument (type) whereas `Passthru` matches any argument list of any length, and the difference between `Passthru` and `Multiple(Any)` is that `Multiple(Any)` collects its argument (types) into a single Array, whereas `Passthru` delivers them as their original list, not collected). Would you prefer to require the `match(Passthru, ...)` for uniformity, or are you OK with allowing this shorthand for when a factory really can handle any argument list? It comes up mainly in generic functions that just "translate" calls to a method into some call or calls to other method(s), such as unequal here being translated into the negation of equal.
glen closed this issue 2025-04-22 05:01:23 +00:00
Owner

Oops, reopening because there is still that one open design question in the comment just prior to this one.

Oops, reopening because there is still that one open design question in the comment just prior to this one.
glen 2025-04-22 05:02:27 +00:00
Owner

A decision on that one open design question is now required for further progress on nanomath.

A decision on that one open design question is now required for further progress on nanomath.
Author
Collaborator

@glen wrote in #12 (comment):

Would you prefer to require the match(Passthru, ...) for uniformity, or are you OK with allowing this shorthand for when a factory really can handle any argument list?

Hi, I have a preference for uniformity, so, requiring match(Passthru, ...). Thanks for asking. Would that be ok with you?

@glen wrote in https://code.studioinfinity.org/StudioInfinity/nanomath/issues/12#issuecomment-2768: > Would you prefer to require the `match(Passthru, ...)` for uniformity, or are you OK with allowing this shorthand for when a factory really can handle any argument list? Hi, I have a preference for uniformity, so, requiring `match(Passthru, ...)`. Thanks for asking. Would that be ok with you?
Owner

OK, next up will be a PR to require explicit match(Passthru,...). I think it would mean that you therefore can't import a value like export const tau = 6.2831853. For putting values into an instance, you will have to write export const tau = match(Passthru, 6.2831853). Is that OK?

Or do you prefer to make a distinction between functions and values -- match required for defining any functions, even functions that apply to any arguments without typechecking, whereas it is not necessary to define non-function values?

Let me know on this point and I will make a PR to nanomath accordingly. Thanks!

OK, next up will be a PR to require explicit `match(Passthru,...)`. I think it would mean that you therefore can't import a value like `export const tau = 6.2831853`. For putting values into an instance, you will have to write `export const tau = match(Passthru, 6.2831853)`. Is that OK? Or do you prefer to make a distinction between functions and values -- `match` required for defining any functions, even functions that apply to any arguments without typechecking, whereas it is not necessary to define non-function values? Let me know on this point and I will make a PR to nanomath accordingly. Thanks!
Author
Collaborator

I think it will be nice if values (constants) like tau or pi can be imported as is, without a wrapper function. They don't have to be matched to anything except their name, and they aren't invoked like a function, so it feels natural to me if they are handled differently than functions. Does that make sense?

I think it will be nice if values (constants) like `tau` or `pi` can be imported as is, without a wrapper function. They don't have to be matched to anything except their name, and they aren't invoked like a function, so it feels natural to me if they are handled differently than functions. Does that make sense?
Owner

Fine by me. I will make Passthru required for all function values, and not for non-function values. Note the current nanomath concept is to use export const tau = match(BigNumber, 6.28318530717958647692528676655900576839433879875021164194988918461) to set the value to be used for tau when config.number is BigNumber (for example), or if you explicitly write math.resolve('tau', BigNumber). In fact, nanomath will be able to arrange that math.tau returns the BigNumber version of tau when config.number is BigNumber, even in straight JavaScript, not just for tau in the parser. Slightly topic creep for this thread, but does that sound like a beneficial direction? It seemed like a natural consequence of the perspective that a "TypeDispatcher" is just like a map, except the keys are strings together with a type list. Then config.number just becomes the default type (~ one-element type list) used when looking up non-function values.

Fine by me. I will make Passthru required for all function values, and not for non-function values. Note the current nanomath concept is to use `export const tau = match(BigNumber, 6.28318530717958647692528676655900576839433879875021164194988918461)` to set the value to be used for tau when config.number is BigNumber (for example), or if you explicitly write `math.resolve('tau', BigNumber)`. In fact, nanomath will be able to arrange that `math.tau` returns the BigNumber version of tau when config.number is BigNumber, even in straight JavaScript, not just for `tau` in the parser. Slightly topic creep for this thread, but does that sound like a beneficial direction? It seemed like a natural consequence of the perspective that a "TypeDispatcher" is just like a map, except the keys are strings together with a type list. Then `config.number` just becomes the default type (~ one-element type list) used when looking up non-function values.
glen closed this issue 2025-04-24 00:16:05 +00:00
Author
Collaborator

It makes sense indeed, it's indeed a good thing to have a high precision version of the constants (when available), and go from there! I think in some cases we can also lazily calculate a constant, like calculating pi via BigNumber.acos(-1).

It makes sense indeed, it's indeed a good thing to have a high precision version of the constants (when available), and go from there! I think in some cases we can also lazily calculate a constant, like calculating pi via `BigNumber.acos(-1)`.
Owner

@josdejong wrote in #12 (comment):

I think in some cases we can also lazily calculate a constant, like calculating pi via BigNumber.acos(-1).

Sure, the thing you export could also be a factory that produces a BigNumber, the framework already allows for that. So good, I will continue with the current design in terms of "type-dependent" constants.

@josdejong wrote in https://code.studioinfinity.org/StudioInfinity/nanomath/issues/12#issuecomment-2810: > I think in some cases we can also lazily calculate a constant, like calculating pi via `BigNumber.acos(-1)`. Sure, the thing you export could also be a factory that produces a BigNumber, the framework already allows for that. So good, I will continue with the current design in terms of "type-dependent" constants.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: StudioInfinity/nanomath#12
No description provided.