Cannot access type inside typed modules

Hello, maybe I am missing something and it is the intended behavior, but why should this not work? It throws The record field parm1 can’t be found..

module type ModuleType = {
  type record
}

module Test: ModuleType = {
  type record = {parm1: string, parm2: string}
}

let record: Test.record = {parm1: "dsfg", parm2: "dsfgdgf"}

It works when the ModuleType type is removed from the definition of the module Test. This is of course very simplified version of the issue and can be simply solved by removing the type. It is just strange behavior from my point of view. And opening the Test module does not help either. It looks like you just cannot access the type inside it, when it is typed.

Will be very thankful for any response or explanation.

This is called abstract type. The shape of the type is not exposed outside of the module.

So for the external world Test.record is a type and no one outside knows the internal representation of this type.

I think I understand the idea of abstract types for the variables and I thought this is kind of the same. That we have module type which describes some restrictions on the module shape and then we have module definition that satisfies this restrictions and implement the type. That is why I do not get why the implementation of the record type in the module Test is shadowed by the abstract type definition of the module. I have stumbled upon this when I was playing with functors and wanted to be more explicit when defining the module that I have passed to functor later. So I have specified the module type for that module and suddenly was unable to define inputs of the functions created in the module created by functor. The example when I found out was something like this.

module type Template = {
  type parmsType
  let toString: parmsType => string
}

module MakeModule = (Template: Template) => {
  let toString = Template.toString
  type parmsType = Template.parmsType
  let print = parms => Js.log(toString(parms))
}

module ModuleTemplate: Template = {
  type parmsType = {parm1: string, parm2: string}
  let toString = ({parm1, parm2}) => `${parm1}${parm2}`
}

module ModuleExample = MakeModule(ModuleTemplate)

ModuleExample.print({parm1: "dfhdfgh", parm2: "dfghdgfh"})

So, now I undestand that it is shadowed, but still do not undestand the reason for that. Could you please elaborate on that?

The reason is that the implementation is hidden by ModuleType. There it is specified as just type record with no other details. So from the compiler’s point of view the Test module exposes a type record with no internal structure. So of course you can’t create a value of the type using a literal–they are not connected. This is a very important property–encapsulation. You will find the same concept in e.g. OOP languages where e.g. a library exposes only an interface but doesn’t allow just constructing values.

How to solve the problem depends on what exactly the end goal is. As you discovered, the simplest way to solve it is to just not give a type to the module. I would say 90% of the time this is the correct solution as it greatly simplifies things. Remember, modules that conform to interfaces automatically have the correct types. You don’t need to specify the type. Type inference works really well.

In some rare cases you do need to annotate the module type and in that case you would most likely want to lift up the record type above the ModuleType and use it below. That way its implementation would not be hidden.

3 Likes

This youtube video explains about Abstract types

2 Likes

Thank you both for the answer. Obviously I am not very well knowledged in terminology, since I imagined something else under the term abstract type. I have mixed it up with generic types. Thank you for shedding light on it.

That youtube video was a great help to better understand the concept and use and I think that it even hinted me where it would be useful, which is the last thing that bothered me a bit. Am I right that giving a module a type or more precisely encapsulating and hiding the implementation by signature is mostly useful in library creation?

That’s right. In a library you want to give interfaces to your modules so that users can easily check the type and documentation.

Thank you. You were a great help to me.

1 Like