Hello!
The other day, I asked for help on the reason discord about how to generate several Units modules without repeating a lot of boilerplate. Someone proposed a great solution that works exactly as I needed. for reference, here it is:
module Unit: {
type t('a);
let (+): (t('a), t('a)) => t('a);
module Make: () => {
type id;
let fromInt: int => t(id);
};
} = {
include Float;
type t('a) = float;
let (+) = (+.);
module Make = (()) => {
type id;
let fromInt = float_of_int;
};
};
Then I can easily use it to create units that are just floats, but that I can’t mix, exactly what I needed:
module Seconds =
Unit.Make({});
module Milliseconds =
Unit.Make({});
While the code works wonderfully, I don’t fully understand it and that is where I want help. I consider that the person that provide this helped me enough already, and I also prefer the forum format to the chat one, so that’s why I’m asking my questioning here. So far I understand the code with my intuition, this means that I see the code and think: yes, this obviously does what I want, I feel it. But when I try to analyse it the details go blurry and I feel confused and frustrated, just like what happens when you try to remember a dream and tell it to someone else. Using something that I don’t understand fully is something my brain does not allow me to do
Sorry if I rubber ducky you a bit, but this is what I understand:
The nested functor Make captures its context, just like a closure will do, so it knows about the fact that t(a)
is just a float (more questions about this latter). Because Make is a functor, it is creating a new version of it’s contents every time it is called, that is how every time you call it you get a different type id
that are not compatible between them, even though they are all just empty.
Maybe my understanding of Ocaml type system is too rigid or archaic, but the thing that confuses me the most is the assignment of type t('a) = float
. For me the type variable should always be filed, otherwise it does not make much sense, but here it is just completely being re-assigned to a plain type
If I put the more abstract hat and I decide that types and code are just two different programming languages I can calm down my anxiety enough to understand how the types work: the Unit.t
type is both, generic and abstract, and the Make.id one is just abstract (phantom, opaque), making them impossible to see from the outside. Make.fromInt
then puts the unique Make.id
into Unit.t
binding this two to create a third type unique to each module, but because we latter declare Unit.t
to be just a float, including the type parameter like this: t('a) = float
, this effectively turns all the types contained on it to also be just plain floats. This is why latter there is no need (probably not even possible) to concrete/declare the type id
at all, it serves no purpose in the “implementation world”, it is just a needed artifact in the types parallel universe.
Am I correct in my understanding?
Is this a common practice? Does it have any particular name? Where can I learn more about neat type tricks like this one?