RFC: Remove Uncurried function types, have uncurried calls by default, opt-in curried

Problem

Functions are curried by default, which leads to

  • overhead in the runtime code (Curry_.5(...))
  • errors in unexpected code parts when the API changes, for example:
  @react.component
  let make = (~onChange:(string) => option<unit => unit>) => {
    React.useEffect(() => {
      let cleanup = onChange("change")
      // cleanup correctly returns option<unit => unit>
      cleanup
    })
    ..
  }
-  let make = (~onChange:(string) => option<unit => unit>) => {
+  let make = (~onChange:(~a: int, string) => option<unit => unit>) => {
  @react.component
  let make = (~onChange:(~a: int, string) => option<unit => unit>) => {
    React.useEffect(() => {
      let cleanup = onChange("change")
      // the error is not at the call site (above), but at the use site (below):
      // "This call is missing an argument of type (~a: int)"
      cleanup
    })
    ..
  }

Uncurried functions on the other hand are

  • inconvenient to write
  • incompatible with curried functions types => it is quite impractical to mix curried and uncurried functions in a code base

Requirements

  • code should be performant by default - it should be clear where there might be a performance overhead
  • the type definition should not care whether a function is curried or not - it is the call site that should make that distinction, for example in this case:
 let make = (~onChange:(string, int) => unit) => {

The caller that passes onChange should not care whether within make it is fully or partially applied:

make(~onChange=((. a, b) => Js.log2(a, b)))
make(~onChange=((a, b) => Js.log2(a, b)))

and vice versa - make should be able to use the type ~onChange:(string, int) => unit both as a curried and uncurried function. This is not a limitation of JavaScript.

In fact - there should not be different definitions for curried and uncurried functions, there should be “functions”:

// can be used both curried and uncurried
let onChange = (a, b) => Js.log2(a, b)

Proposal

  • It should be the call side, not the type, that decides whether you want to apply all arguments to a function or not. All functions and function types are defined the same:
type onChange: (string, int) => unit
  • The default function call syntax becomes ‘uncurried’ - calling a function onChange(a, b) requires you to provide all arguments, or an error is triggered - this is the expected behaviour in all languages that people are familiar with (and that ReScript wants to cater to)
  • You can opt-in curried behaviour, for example calling onChange(. a) or onChange(.. a) results in a new function (int) => unit:
let make = (sum: (int, int) => int) => {
  // error: This function expected 2 arguments, but got 1:
  let x = sum(1)
  // okay, in this case x = int => int
  let partiallyApplied = sum(.. 1)
}
  • calls with the fast pipe -> are curried by default → there is no change in the behaviour of piping
// works
let x = sum(1, 2)->sum(3)->sum(4)

What are your thoughts?
(I fully realize that the current state is what OCaml is doing, but with the departure from native, this seems to be a more inline with the JavaScript world)

1 Like

There was a big discussion around this almost exactly a year ago:

2 Likes