Confused about a type variant scope

Hello,

Please see the following code:

module User = struct
  type first_name = FirstName of string
  type last_name = LastName of string

  let with_first_name str = FirstName str
  let with_last_name str = LastName str
  let greet (FirstName fn) (LastName ln) = "Hello " ^ fn ^ " " ^ ln ^ "!"
end

let _ =
  print_endline @@ User.greet (FirstName "John") (LastName "Doe");
  let jane = User.with_first_name "Jane" in
  let doe = User.with_last_name "Doe" in
  print_endline @@ User.greet jane doe
;;

I’m surprised about this line:

print_endline @@ User.greet (FirstName "John") (LastName "Doe");

Why am I able to call into FirstName without specifying its module?

If I want to extract the param, I have to fully qualify the function:

let john = User.FirstName "John" in
print_endline @@ User.greet john (LastName "Doe");
...

I think that is type inference at is best. Ocaml is able to figure out, because there is no other thing in scope, which variants you are referring to. I don’t have a deep understanding, but I know that types and values are on separate namespaces, so the fact that one needs to be scoped does not guarantee that the other needs it

I believe this is a case of “type-directed disambiguation”; basically the typechecker uses contextual information (in this case, the signature of User.greet to resolve constructor (and record label) names.

In the second case, let john = FirstName "..." in ... there isn’t enough “contextual information” to disambiguate FirstName.

Cheers,
Nicolas

Incidentally, if you want to learn more about the history of this feature and why it works the way it does, there’s plenty of good reading material:

Cheers,
Nicolas

4 Likes

Indeed, this is type-directed disambiguation at work. If enabled the warnings name-out-of-scope and disambiguated-name provides more information:

User.greet (FirstName "John")

Warning 40 [name-out-of-scope]: FirstName was selected from type User.first_name.
It is not visible in the current scope, and will not
be selected if the type becomes unknown.
Warning 42 [disambiguated-name]: this use of FirstName relies on type-directed disambiguation,
it will not compile with OCaml 4.00 or earlier.

3 Likes

I wouldn’t have thought of it :+1:

Thanks for those links, I appreciate it!

Good to know. Is using this feature considered good practice? Or would you say turning on all warnings is probably a better option for a new comer to the language?

Having read a good part of the links provided by @nojb, I couldn’t make out if it was generally accepted as a good feature to use, or just something nice to have for quick prototyping.

I would say that it is a good feature to be used without restriction :slight_smile:

The only downside I can think of is that it may sometimes complicate moving code around because you can take a piece of code that typechecks in a given context, but once you move it to some other place it no longer does due to insufficient contextual information. But I don’t think this is a big issue in practice.

Cheers,
Nicolas

fwiw you can replace:

print_endline @@ User.greet (FirstName "John") (LastName "Doe");

with:

print_endline User.(greet (FirstName "John") (LastName "Doe"));

and completely remove ambiguity.

I like using this disambiguation feature in constructor pattern matches. Allows a lot less repetition.

1 Like

I find it’s curious that this case surprisingly works… while others which seem, to me, less ambiguous don’t.

e.g. I have some code like:

  let roots = List.map O.of_list pts in
  fun () ->
    let open O in
    List.map (fun root ->
        List.map (fun pt -> nearest root.tree pt) targets
      ) roots

in this code the type of roots is O.t list where O.t is a record type

but I can’t do root.tree without opening the O module, despite the type inference (at least the one in VS Code) seems to be fully aware that root is an O.t record.

I guess there is some technical reason, but naĂŻvely it seems like this should be inferable without the explicit module opening.

The type of root is not yet known when the typechecker encounters root.tree in your code.
The typechecker thus cannot use type information to find the tree field.
This is the reason why adding an annotation:

fun (root:O.t) -> ... root.tree ...

removes the need to open the O module.

But you are right that type-directed disambiguation is sensitive to the precise flow of type information inside the OCaml typechecker (aka it is not principal) which is one of the reason why the current implementation of type-directed disambiguation is not considered to be satisfying at the theoretical level.

1 Like

Ah yes, that works!

But it’s interesting that without either the explicit open or the extra annotation, if I hover over the root var in VS Code it’s able to tell me it’s an O.t.

It’s not clear to me why the type of root would not be known if the type of roots is known - seems like the signature of List.map should ensure that it’s known too.

The type of root is not yet connected to the type of roots at the time the typechecker is looking at root.t because the type of the argument is checked first.

It this reliance on typechecking time that makes type-directed disambiguation not completely satisfying on the theoretical level.

Gotcha

Thanks for the tip! That one’s awesome!!