Hi @mishaszu,
Your issue is that the type Schema.T.t is abstract. This is often used for “data-hiding” purposes but it also disallows the compiler of “looking inside” the actual implementation. Therefore you can’t access the record type later on. (in your current solution)
Edit: Sorry for the confusion: What I refer to as abstract type
is the same concept @spyder refers to as opaque type
. Which means I’m propably using incorrect nomenclature.
I’ll try to further explain myself:
Long explanation
Given the following module type and implementation:
module type T = {
type t
let toString: t => string
}
module M = {
type t = {name: string}
let toString = t => t.name
}
Since the module type T is not explictly used in the given code, the compiler infers the signature of module M
as follows:
module type M = {
type t = {name: string}
let toString: t => string
}
Coincidentally, this is similar (!) to module type T
. The notable difference is type t
being abstract vs the record signature. This means, you can access anything inside of module M
anywhere else in your code base.
e.g.
module A = {
let exclamation: M.t => string = x => x.name ++ "!"
}
Note, that you can use the type M.t
as well as the “implementation knowledge” of type t
being a record with a single field called name
.
Now take a look at the following example (using the module type now),
which will yield a compile-error The record field name can't be found.
when we try to access a record field of type t
:
module type T = {
type t
let toString: t => string
}
module M: T = {
type t = {name: string}
let toString = t => t.name
}
module A = {
let exclamation: M.t => string = x => x.name ++ "!"
}
This is the exact issue you were facing (but without the complexity of a functor):
The line module M: T = ...
explicitly defines the module M
having a module singature of module type T
. (How this module is seen from the outside.)
Therefore, you are preventing any other code, than the module M
itself, to actually know the shape of type t
. To the outside of the module, we just know, there is a type
called t
defined in the module M
, but we can only use values of this type with functions, which use this type.
This is actually data hiding
.
Note: In many scenarios, this behavior is desirable.
Now looking at module types
from another perspective: They can also be used in roughly similar ways, like typeclasses
(in e.g. Haskell) can be used to support generalization
:
If you define an argument having module type T
, you are saying “I can pass any module to this function, as long as it’s signature is a superset of module type T
”. (meaning the actual implementation of the passed module could have a lot more types, functions and values defined, but you are only caring about those, mentioned in module type T
.
Now see the following functor:
module type T = {
type t
let toString: t => string
}
module M = {
type t = {name: string}
let toString = t => t.name
}
module Exclamation = {
module Make = (X: T) => {
let exclamation: X.t => string = t => t->X.toString ++ "!!!"
}
}
module Test1 = Exclamation.Make(M)
let test2 = Test1.exclamation({name: "test"})
The functor’s argument module X
is required to have module type T
, therefore, the functor cannot know the actual shape of the type X.t
, but the type itself can still be used.
Since the signature of module M
is infered, the shape of type M.t
is publicly known and can be used to create a value being passed to the exclamation
function.
Note: If you would explicitly define module M to have the signature of module type T
, you couldn’t create (or use the shape of) a value of M.t
. - Which is, what you experienced!
Short Solution
To give a answer after the rather long explanation:
Just remove your explicit signature definition for Wrapper.Test and it will compile successfully:
let rawValue = %raw("{a: {name: 'test'}}")
module Schema = {
module type T = {
type t
let extract: 'a => t
}
module Make = (Item: T) => {
let return = (val): promise<Item.t> => Item.extract(val)->Js.Promise.resolve
}
}
module Wrapper = {
module Test = { // <--- module type `: Schema.t` got removed here
type t = {name: string}
@get external extract: 'a => t = "a"
}
module MyTest = Schema.Make(Test)
}
let item = Wrapper.MyTest.return(rawValue)
let _ = item -> Js.Promise2.then(async val => {
Js.log(val.name)
})
“Masterclass”
Some additional information, if you desire to take more time to play around with these concepts:
The above defined functor explicitly defines the argument’s module type
, but the compiler will infer the the module type returned by the functor.
While this may work for you, it is possible to explicitly annotate the return type as well:
module Schema = {
module type T = {
type t
let extract: 'a => t
}
module type Schema = {
type t
let return: 'a => promise<t>
}
module Make = (Item: T): Schema => {
type t = Item.t
let return = (val): promise<Item.t> => Item.extract(val)->Js.Promise.resolve
}
}
Now we have a new (but similar) problem:
The functor’s returned module has an abstract type t
, which may not be desired.
“(destructive) replace” to the rescue:
module Schema = {
module type T = {
type t
let extract: 'a => t
}
module type Schema = {
type t
let return: 'a => promise<t>
}
module Make = (Item: T): (Schema with type t := Item.t) => { // <-- Note the extra with ... syntax for the return type
let return = (val): promise<Item.t> => Item.extract(val)->Js.Promise.resolve
}
}
In this case the module type Schema
serves kind of as a “template” and we use “destructive replace” to remove the type definition Schema.t and replace any occurence within the module type with the type reference to Item.t
.
Different Approaches
Depending on your use-case it is often easier to completely avoid the hassle of functors und use either First Class Modules or other architectural patterns.