Why do I need to open a module to refer to fields on one exported type?


I’m just exploring the code generated by spin fro a basic CLI application and I found something that I don’t understand.
There is the module error wich contains this:

type error = {
  doc: string,
  exit_code: int,

let all = () => [
  {doc: "on missing required environment variable.", exit_code: 201},

Then on some other part of the codebase, there is this line:

    |> List.map(~f=el =>
         Reason_ls.Errors.(Term.exit_info(el.exit_code, ~doc=el.doc))

I was asking myself what was the opening of the Errors module for… after all, the el var has all the things you may need… So I deleted it to discover that you can not access the fields of a type defined in another module even if you have a reference to an instance of that type.
Why is this? What is the reasoning behind that? Does it happen for everything?


Reason infers the types of records and variants inside function bodies by looking for the field or constructor names inside some module that is in scope. This means that whatever module defined the record or variant needs to be opened for its fields and constructors to be visible for type inference to work.

There are a few different ways to open modules in Reason, but locally opening modules like Reason_ls.Errors.(...) is considered one of the safest and easiest to read, because it brings as little as possible into the general scope.

Another way to open the module for record type inference is to open it directly before the field access, like:

Term.exit_info(el.Reason_ls.Errors.exit_code, ~doc=el.doc)

(Note that you only need to prefix one field for type inference to kick in for the others in the same expression).

This is my preferred way of opening modules for record and variant type inference.

A simple note on pipe first and pipe last

Thank you for the explanation, makes sense and thanks for the tip.
So if I annotate the variable or the function return I will not need to open the module?


Annotating the parameter type or the return type works sometimes, but not always. It depends on exactly what’s happening inside the function body. Also, it’s not really idiomatic to type-annotate Reason implementation code (we have interface files for that). Using the module name prefix method I showed is a foolproof way to get good type inference. I would recommend using that.


Cool, thanks!