Why is manual type annotation required on the arguments of some functions, specifically records?


#1

Here is an example on sketch: https://sketch.sh/s/m8DJVHrcuVn8ujDxN2gBq7/

If you look at the last module (Logic), you’ll see that the arguments to the function in the liftA2 call are annotated like ({foo}: Foo.t, {bar}: Bar.t) => Baz.{foo, bar}. If I remove all (or even just one) of the annotations (({foo}, {bar}) => {foo, bar}), the function throws compiler errors. If I change those types from records to strings or ints, this is no longer the case.

Is there a reason for this? Is there somewhere in some docs I can find more information about this? Is there something I should be looking out for that I may be doing incorrectly?


#2

Hi, this is explained here: https://reasonml.github.io/docs/en/record#record-needs-an-explicit-definition

This is why this part of your code: Baz.{foo, bar} works without an explicit annotation. It prefixes it record literal with the module where the fields may be found. The compiler will also infer the type correctly if you prefix just one field with its module, so e.g.:

module Logic = {
  let action = (foo, bar) =>
    Opt.liftA2(
      ({Foo.foo}, {Bar.bar}) => {Baz.foo, bar},
      foo,
      bar,
    );
};

#3

I have 2 problems with that answer:

  1. What you’ve done above is also an explicit annotation, just in a different style (according to the linked examples).
  2. That answers seems to be for a different question.

What I want to know is why I need to annotate the values on line 35 even though they are already explicitly annotated on line 32.


#4

What I want to know is why I need to annotate the values on line 35 even though they are already explicitly annotated on line 32.

My guess would be that you’ve already explicitly annotated the lvalue, which is not enough information for OCaml to infer the types inside the rvalue. But it works the other way round, as per Yawar’s example.

But then I’m no expert :blush:


#5
  1. Yes, the annotation is required in one style or the other.
  2. I think it does answer the question, maybe the wording can be improved a bit. Right now it’s:

If the type definition resides in another file, you need to explicitly indicate which file it is: … Either of the above 3 says “this record’s definition is found in the School file”. The first one, the regular type annotation, is preferred.

I actually disagree with the last sentence, I think the module prefix of a field name should be preferred.

Inference of record types is a bit limited as you can see. The typechecker doesn’t dive into modules to find the record type definitions. But with a prefix on any field, the ambiguity goes away and you can get away with fairly minimal annotations.


#6

Well, the upside of the me: School.person is not only that it’s more robust (it’s a bit of a smell, but a module can contain more than one record type with a given field name), it’s also more descriptive. School.person as an example is a bit convoluted, but if we go with it, you arguably want to describe a value as a «person», not as, «oh well, something to do with school».

P.S. Unless, of course, your modules are so atomic as to have types like Student.t. Student.t is no more descriptive than just Student.


#7

Yes, the idiom is to prefer atomic modules for as many types as possible. It’s especially helpful with record types. See https://v1.realworldocaml.org/v1/en/html/records.html#reusing-field-names :

We can avoid this ambiguity altogether, either by using nonoverlapping field names or, more generally, by minting a module for each type. Packing types into modules is a broadly useful idiom (and one used quite extensively by Core), providing for each type a namespace within which to put related values. When using this style, it is standard practice to name the type associated with the module t.