Was working through an example of a design pattern called Constructor + View by the author that was explained through types but was having trouble figuring out the implementation.
This is the module signature:
module User : {
type t;
type view = { name: string, age: int };
let make: (~name:string, ~age:int) => option(t);
let view: t => view;
};
So User.t
is hidden but from one function you can pattern match the user record
At first was thinking User.t
and User.view
could have the same fields:
module User: {
type t;
type view = { name: string, age: int, };
let make: (~name: string, ~age: int) => option(t);
let view: t => view;
} = {
type t = { name: string, age: int, };
type view = { name: string, age: int, };
let make = (~name, ~age) => Some({name, age});
let view = t => {name: t.name, age: t.age};
};
But got an error that looks like it can't tell the difference between view
and t
:
Values do not match:
let make: (~name: string, ~age: int) => option(view)
is not included in
let make: (~name: string, ~age: int) => option(t)
Tried a couple more things, the first just taking out make
and trying to get the view
function to work but same issue:
module User: {
type t;
type view = { name: string, age: int, };
let view: t => view;
} = {
type t = { name: string, age: int, };
type view = { name: string, age: int, };
let view = t => {name: t.name, age: t.age};
};
with error:
Values do not match:
let view: view => view
is not included in
let view: t => view
The second try was for having the view
type be a subset of the fields (which was the use case I was hoping to use this pattern) but this has the same error as above:
module User: {
type t;
type view = { name: string, age: int, };
let view: t => view;
} = {
type t = { name: string, age: int, email: string };
type view = { name: string, age: int, };
let view = t => {name: t.name, age: t.age};
};
My question here is if there's a way to implement something to fit the first module signature with User.view
being the same fields or subset of fields as User.t
? Can make it work if the records have different fields or if I separate the records by modules but curious about that specific use case.
Records are nominal, not structural types. So it's not enough that the type looks the same, the compiler actually has to infer the exact type definition, which is of course impossible if the two types are identical. But even if they aren't identical the compiler will struggle with fields of the same name and just go with the first match it finds, which is the last type defined.
In your case this isn't a problem externally, since only view
is exposed. But internally you have to help the compiler out with a few type annotations. This compiles:
module User: {
type t;
type view = { name: string, age: int, };
let make: (~name: string, ~age: int) => option(t);
let view: t => view;
} = {
type t = { name: string, age: int, };
type view = { name: string, age: int, };
let make = (~name, ~age) => Some({name, age}: t);
let view = (t: t) => {name: t.name, age: t.age};
};