Skip to content
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

Add return-type annotations? #124

Open
gwhitney opened this issue Mar 7, 2022 · 9 comments
Open

Add return-type annotations? #124

gwhitney opened this issue Mar 7, 2022 · 9 comments

Comments

@gwhitney
Copy link
Collaborator

gwhitney commented Mar 7, 2022

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.

@josdejong
Copy link
Owner

👍 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.

@gwhitney
Copy link
Collaborator Author

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).

@gwhitney
Copy link
Collaborator Author

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.

@josdejong
Copy link
Owner

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 })
]

@josdejong
Copy link
Owner

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
}

@gwhitney
Copy link
Collaborator Author

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 of Returns just because it needs to be written so many times, but my guess is you'd prefer written-out Returns?
  • 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".

@josdejong
Copy link
Owner

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 :)

@TheOneTheOnlyJJ
Copy link

Is there any other progress on this?

@josdejong
Copy link
Owner

No

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants