reason icon indicating copy to clipboard operation
reason copied to clipboard

Typing unary functions has visually similar syntax and leads to confusing compiler errors.

Open pckilgore opened this issue 6 years ago • 0 comments

Part of the joy of the great type inference we have with Reason is that it might be quite a while before users explicitly have to annotate the type of a function's argument.

This joy, however, leads to some gotchas.

For example, one will get used to writing unary functions like this:

let someFunc = arg => {/* ... */};

Two things:

  1. Note arg doesn't here (and in my experience, usually won't ever) require a type annotation. This is usually great!
  2. refmt will convert ... = (arg) => ... to ... = arg => ..., even if you tried to adopt a more verbose syntax yourself.

Together, these lead to a very easy to encounter, yet hard to debug, miscommunication between programmer and compiler.

For example, the simple code:

type dog = {
  name: string,
  isGood: bool
};
let petDog = dog =>
  dog.isGood ? Js.log2("Pet", dog.name) : Js.log("Impossible!");

All is good!

Later type dog is extracted to its own module, but petDog still exists.

module Dog = {
  type t = {
    name: string,
    isGood: bool,
  };
  let exampleDog = {name: "Spot", isGood: true};
};

let petDog = dog =>
  dog.isGood ? Js.log2("Pet", dog.name) : Js.log("Impossible!");

Compiler error!

We've found a bug for you!
OCaml preview 10:13-18

The record field isGood can't be found.

If it's defined in another module or file, bring it into scope by:
- Annotating it with said module name: let baby = {MyModule.age: 3}
- Or specifying its type: let baby: MyModule.person = {age: 3}

But this makes sense, and is super helpful. Let's fix it by annotating the function argument so the compiler knows that dog is really type Dog.t.

module Dog = {
  type t = {
    name: string,
    isGood: bool,
  };
  let exampleDog = {name: "Spot", isGood: true};
};

let petDog = dog: Dog.t =>
  dog.isGood ? Js.log2("Pet", dog.name) : Js.log("Impossible!");

Same error ?!?! Wait, I thought we fixed this!

We've found a bug for you!
OCaml preview 10:14-19

The record field isGood can't be found.

If it's defined in another module or file, bring it into scope by:
- Annotating it with said module name: let baby = {MyModule.age: 3}
- Or specifying its type: let baby: MyModule.person = {age: 3}

Now, seasoned Reason programmers might spot what we actually did here to cause this confusion, but I'd wager that this one might even trip a few of them up:

We typed the return type of the function, not the argument!

We really wrote this (note, refmt will strip the parens below to result in the name code as above!)

let petDog = (dog): Dog.t => {/*...*/}

when we should have written:

let petDog = (dog: Dog.t) => {/*...*/}

The difference is really hard to spot after refmt, especially given programmer expectations.

let petDog = dog: Dog.t => {/*...*/}

(wrong)

let petDog = (dog: Dog.t) => {/*...*/}

(correct)

Note, this is much less confusing around higher-arity functions because the required parens around the arguments makes the difference between typing an argument and typing the return type of the function very clear.

What Should Have Happened

I think the easiest solution here is for refmt to require parens around the arguments to unary functions in the = (arg) => syntax. It is a bit noisier, to be sure, especially since most unary functions will not require a type annotation. But when a type annotation is required, it is very easy to determine the appropriate place for it to go.

The harder solution (and I do not know the feasibility of this) is to detect = arg: type => when parsing and emit a warning along the lines of

The Reason compiler has interpreted this annotation as the return type 
of this function, are you sure you didn't mean to annotate the type of this
function's argument?

If you mean to annotate the argument type, you need to wrap the 
argument name and the annotation in parentheses:
`(argument: type) =>` instead of `argument: type =>`.

Ideally that warning would only emit when the relevant function actually contains a problem related to typing.

pckilgore avatar Nov 23 '19 16:11 pckilgore