Both features are unrelated, as far as I know. My mental model is to think of phantom types in terms of visibility, and about generics in terms of generalization.
For example, here is a very small case where a basic phantom type (i.e. non generic) is used to validate a string:
module Validator: {
type t;
let isValid: string => option(t);
} = {
type t = string;
let isValid = (s) => s === "valid" ? Some(s) : None;
};
This is a simplified example, but one could apply any validation to that input type. Note the module type definition doesn’t specify what t
actually is (that is how a type becomes “phantom”), while the implementation specifies that t
is a string
.
So, inside the Validator
module, t
is equivalent to string
. But outside of validator, it’s just Validator.t
. If you try to use Validator.t
as a string outside the module, the compiler will complain (example). And the other way around, you can build a bunch of functions that accept only “validated strings” (i.e. values of type Validator.t
).
With phantom types, one can guarantee at the type system level that a specific piece of data has gone through some given functions in a specific order, which can be very useful in many scenarios.
After this, one might want to write a Validator
module that validates different types (not only strings), so phantom types can be combined with generics, in the spirit of the ReasonTraining article that was linked above. But basic phantom types should go a long way.
I hope this helps!