As a beginner I got stuck on this for a long time while experimenting with external bindings. I though I was misunderstanding external bindings when in fact I was misunderstanding something that looks so simple. (not the binding code below, just a minimal example)
This works
let startAt = () => 1
let plus1 = x => x + 1
let end = startAt()->plus
But this results in an error
let startAt = () => 1
let plus1 = x => x + 1
let end = startAt()->plus1() // It only accepts 1 argument; here, it's called with more.
What is lurking here? Is something else being passed?
Something else that may help this make sense: there are no zero-argument functions in ReScript. f() is really just a special syntax for f(()), which is a function which takes one () unit argument.
When you write a->f(), the syntax transform roughly goes like this:
I hoping yes. “->” will always take the output of f1(), even if its (), and “unshift” it into the arguments of f2, including the hidden () of f2. But I can’t help thinking there’s some deeper rules going on here, or hidden sugar. Like in the Curried functions section here
Is a () in the definition of a function only allowed because it will terminate optional parameters or is there something bigger here. I just tried and obviously this doesn’t work
() is just the value of the unit type, exactly like how 1 is a value of the int type. unit is special though because it only has a single value, ().
The reason why (x, 1) => ... doesn’t work is because 1 isn’t exhaustive; there are other int values that could be passed to the argument. However, (x, ()) => ... works because () is exhaustive; there are no other possible values that can be passed to the argument besides ().
One thing to note is that the only thing special about () is its syntax. Otherwise, it works just like a regular variant. You can replicate its behavior with a custom type:
type unit2 = Unit
let f = (x, Unit) => x
Another thing to understand is that function arguments are patterns, just like in the pattern matching that you can do in switch statements. This means you can do more complex pattern matching too:
type t = A(int) | B(int)
let f = (A(x) | B(x)) => x
f(A(1))
f(B(1))
// This won't compile though:
let f = (A(x)) => x // You forgot to handle a possible case here, for example: B
This is the same reason why destructuring records and tuples works inside function arguments, since destructuring and pattern matching are the same thing in ReScript.
So when you write (x, ()) => ... you’re saying "bind the first argument to x, and destructure the second argument as ()".