Is f1()->f2() always a type error?

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?

It just doesn’t seem consistent that

let f = (x,y) => x+y
1->f(2) // OK

But

let f = (x) => x+1
1->f() // BAD
1 Like

Hi @kswope

In ReScript () is called unit. Unit would be helpful in functions which accept Optional Labeled Arguments
So

startAt()->plus1()

is same as

plus1( startAt(), () )

The above code throws an error since plus1 accepts a single argument, but two were being passed to it.

startAt()->plus does not throw an error because it is same as plus( startAt() ).

4 Likes

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:

  1. a->f()
  2. a->f(())
  3. f(a, ())

Which brings us to the type error.

6 Likes

I need closure on the original question

Is f1()->f2() always a type error?

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

It’s possible for it to not be a type error if f2 accepts two arguments and the second argument is unit ().

let f1 = () => 1
let f2 = (a, ()) => a + 1
f1()->f2() /* This is the same as f2(f1(), ()) */

But in practice there’s rarely, if ever, a reason to write functions like that.

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

let f1 = (x, 1) => x

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

2 Likes