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