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.