API to specify a function name and multiple signatures in a single file #12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
And then, to implement multiple signatures:
Or maybe rename
onType
intocreate
orfactory
, for example:Thinking about it, it could also just be wrapping signatures in an array:
What do you think?
Well currently I just allow
(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 repeatonType
for each type pattern?Or if you like a list of pairs better, we could switch to
or something like that.
Another alternative, if you don't mind repeating a function symbol, would be:
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 thisadd
definition gets merged into the TypeDispatcher, it looks up the keys of the object associated withadd
in that global repository to find the TypePatterns it should use. Don't get me wrong, the string returned bypattern
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.
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 likeonType("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: everyonType
contains just a single signature, it's very explicit and I think easier to read than adding more (and nested) brackets.Would that be ok with you?
@josdejong wrote in #12 (comment):
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.
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, likeon(NumberT, Optional(BooleanT))
ormatch(NumberT, Optional(BooleanT))
?Then we can either do
Or
Whichever you prefer -- let me know. And either way is it OK if we also allow
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.
😂 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 likeon('click', ...)
).My preference is to allow both an array with
on(...)
and a singleon(...)
:I guess you still want to keep the first argument of
on
an Array with the types of the arguments of the function, right?@josdejong wrote in #12 (comment):
I actually do like a linter that complains about some egregious things, without changing anything.
Agreed.
@josdejong wrote in #12 (comment):
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 (likeOptional(BooleanT)
rather than types, it's just that types are automatically converted into patterns that match themselves. So if you prefermatch
, I'm totally fine with that. Another option would bewith
. Just let me know.Got it.
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?Yeah, good point, I think
match
is better thanon
. De dispatcher is indeed "just" doing pattern matching :). Ok let's go formatch
then.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.No sorry I was not being clear:
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?Ah, ok, yeah that sounds good to me. It is quite a common case indeed so nice to have.
OK, I am working on this now, and just wanted to mention that at the moment, it works to simply export a "bare" factory:
This means: use the associated factory for
unequal
no matter what the actual arguments and their types are. It is precisely a shorthand for(where the difference between
Passthru
andAny
is thatAny
matches a single argument (type) whereasPassthru
matches any argument list of any length, and the difference betweenPassthru
andMultiple(Any)
is thatMultiple(Any)
collects its argument (types) into a single Array, whereasPassthru
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.Oops, reopening because there is still that one open design question in the comment just prior to this one.
A decision on that one open design question is now required for further progress on nanomath.
@glen wrote in #12 (comment):
Hi, I have a preference for uniformity, so, requiring
match(Passthru, ...)
. Thanks for asking. Would that be ok with you?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 likeexport const tau = 6.2831853
. For putting values into an instance, you will have to writeexport 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!
I think it will be nice if values (constants) like
tau
orpi
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?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 writemath.resolve('tau', BigNumber)
. In fact, nanomath will be able to arrange thatmath.tau
returns the BigNumber version of tau when config.number is BigNumber, even in straight JavaScript, not just fortau
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. Thenconfig.number
just becomes the default type (~ one-element type list) used when looking up non-function values.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)
.@josdejong wrote in #12 (comment):
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.