I want to offer a counterpoint to using a module prefix to disambiguate records and variants. I think itâs convenient sometimes, but itâs also fragile. Consider this basic example:
module Either = { type t<'a, 'b> = Left('a) | Right('b)}
let f = x => switch x {
| Either.Left(x) => Js.log2("left", x) // Module prefix
| Right(x) => Js.log2("right", x)
}
This compiles fine and is readable. However, suppose we decide to swap the order of the switch
paths:
let f = x => switch x {
- | Either.Left(x) => Js.log2("left", x)
| Right(x) => Js.log2("right", x)
+ | Either.Left(x) => Js.log2("left", x)
}
We get this error:
[E] Line 7, column 4:
The variant constructor Right can't be found.
Since the compiler checks from top to bottom, left to right, it encounters Right
before it encounters Either.Left
, so it doesnât know that Right
comes from the Either
module.
This trivial example is easy to fix, but it becomes a headache when youâre refactoring huge functions. Sometimes the module prefixes could be somewhere deep down one path, and may seem unnecessary until theyâre moved.
But this issue never comes up if you just use a type annotation.
let f = (x: Either.t<_>) => switch x {
| Right(x) => Js.log2("right", x)
| Left(x) => Js.log2("left", x)
}
Using module prefixes only for the sake of avoiding type annotations seems like just trading one kind of annotation for another, and doesnât seem like much of a win to me.
(Module prefixes also arenât able to disambiguate when constructors or record fields are shadowed within a module, but IMO you shouldnât be shadowing those to begin with.)
And I want to be clear that I donât believe that one of these styles is necessarily superior to the other. Sometimes itâs simpler to add a module prefix, and sometimes you want the robustness of the full annotation. I use both in my own code, but lately Iâve been biased towards using annotations more.