Add return-type annotations?
There are two circumstances so far in which the utility of typed-function knowing the return type of (each signature of) a typed-function has come up: (1) supporting useful type inference in automatically-generated TypeScript declarations for typed-functions, see #123, and (2) determining what sort of an entity a sub-expression in math js will be, so as to know what simplifications are valid to apply to that subexpression. (Right now, mathjs just applies the same configurable list of possible simplifications at all levels of an expression, but that could be refined to allow changing the order of numbers being multiplied but leave the order of matrix multiplications alone, if mathjs could tell which parts of an expression evaluated to numbers or to matrices.)
Given these two indications of its value, it's worth considering whether to add return-type annotations to typed-function.
👍 makes sense. I guess it is only relevant in a TypeScript context, so it may be enough to just rely on TypeScript definitions and not something similar to the map with JavaScript (input) signatures that typed-function generates right now.
I think with the advent of a very promising proposal in #123 that looks like it would actually work, the priority for this issue rises. So there becomes the question of notation. Would we want to put the return type in the object key like:
const sqrtImps = {
'number|Complex -> number|Complex' : imp1,
'BigNumber -> BigNumber|undefined' : imp2
}
Or put them in the value, like:
const sqrtImps = {
'number|Complex': Returns('number|complex', imp1),
BigNumber: Returns('Bignumber|undefined', imp2)
}
The former is prettier but the latter would almost certainly be easier to deal with in the code, so i think i come down on that side personally. This also begs the question of how to deal with an operation like sqrt whose return type depends on config variables (although organizing as in the Pocomath prototype would solve that, as the implementation is not returned until the config is read).
And actually, another reason why this is not only relevant in a TypeScript world and hence is well worth doing (quoted from https://code.studioinfinity.org/glen/pocomath/issues/52):
- provide a compose function:
math.compose(math.sqrt, math.negate)which produces a function that computes sqrt(-x) but only performs type dispatch once, when negate gets the argument, but arranges to call the proper implementation of sqrt on the result without having to typecheck. This compose operation could be used to provide the semantics of expressions, nearly eliminating the typechecking that would go on in evaluating an expression.
Agree, I find a notation in the signature, like 'number|Complex -> number|Complex' : imp1 better readable, but if this makes it complex to implement a notation like 'number|Complex': Returns('number|complex', imp1) best.
Thinking aloud, if we go for the second approach, maybe it is possible to go for a notation like:
const sqrtImps = [
['number|Complex', 'number|complex', imp1],
['BigNumber', 'Bignumber|undefined', imp2]
]
or something like:
const sqrtImps = [
signature({ args: 'number|Complex', returns: 'number|complex', fn: imp1 }),
signature({ args: 'BigNumber', returns: 'Bignumber|undefined', fn: imp2 })
]
As for the first approach, we could possibly write that in a TypeScript compatible notation:
const imps = {
'sqrt(x: number|Complex) : number|Complex)' : imp1,
'sqrt(x: BigNumber) : BigNumber|undefined' : imp2
}
I am in the middle of implementing a proof-of-concept of return-type annotations in https://code.studioinfinity.org/glen/pocomath (but I am also still in the middle of the continent, so it won't be done for a bit) and based on the experience so far:
- Note there can be at most one implementation for a given input signature. It makes no sense to have one implementation that takes a number and returns a string and a different implementation that takes a number and returns a boolean. With keys that include both the input signature and return type like 'number -> string' or 'myoperation(x: number) : boolean' it might seem plausible that you could have implementations that differ only in return type (or in the latter case, the name of the input parameter as well). In summary, the input signature is the unique identifier of an implementation, so it makes a natural key for a plain object of implementations. (i.e. the "second approach" from above seems more natural).
- As far as your suggested notations for the second approach go, it feels just slightly mismatched that they are Arrays of implementations, because that feels like it implies that repetition might be ok and that the order matters, whereas in fact generally speaking typed-function reorders the provided implementations (and only rarely falls back to the order they are specified in). Are there drawbacks to the notation I suggested for the second approach (labeled "put them in a value" in my comment)? So far in the prototype I actually used
R_in place ofReturnsjust because it needs to be written so many times, but my guess is you'd prefer written-outReturns? - Perhaps most importantly, I reached the conclusion that if we go in a Pocomath-like direction in which the dependencies are listed on each implementation, then the return type has to come after the dependencies, because it can depend on the dependencies, as in:
export const sqrt = {
number: ({config, complex}) => {
if (config.predictable) {
return R_('number', n => Math.sqrt(n))
}
return R_('number|Complex', n => {
if (n>=0) return Math.sqrt(n)
return complex(0, Math.sqrt(-n))
}
}
}
where the return type depends on the configuration, and in
export const negate = {
number: ({T}) => R_(T, n => -n)
}
where the return type depends on the actual input type T that is captured by the implementation (because Pocomath has a subtype NumInt of number for numbers that happen to be integers, and we need to capture the fact that the negation of a NumInt is a NumInt without replicating the implementation, which is good for both the NumInt subtype and regular number type). So specifically in the Pocomath style, only the second approach is feasible since the return type has to be determined inside the outer layer that takes the dependencies. I'll let you know when this aspect of Pocomath is "done" so you can take a look; let me know if you want me to replace all occurrences of "R_" with "Returns".
Very good points, thanks for the clear explanation.
Yes I prefer Returns (or returns) over R_, it looks less magical and a stranger will immediately have an idea what it means :)
Is there any other progress on this?
No