Modules allow us to encapsulate business logic like let bindings or types into logical entities. In ReasonML every file is a module. Names have to be unique though per project. This is quite opinionated compared to other languages. Sub-modules also can be created using the module
keyword. In addition to that ReasonML supports interface files that not only describe let bindings, functions and their types, but also enforce visibility of those. In this lesson we explore all of these features step by step without getting overwhelmed.
Instructor: [00:00] To create a module, use the keyword module followed by the name. The module name must start with a capital letter. Then we have the module block. The purpose of a module is to encapsulate let bindings, type definitions, nested modules, and so on. For example, for our math module, it would contain P as well as an add function.
[00:26] Using the dot notation, we can access everything contained inside a module. As mentioned, we can store types and access them using the dot notation. This also means by default for type inference the compiler doesn't look into other modules than the current one or parent modules.
[00:49] Constantly referring to a value and type in another module can be tedious. To help with that, Reason allows us to open a module's definition and refer to its contents without prepending the name every time.
[01:03] For once we can open locally by using the module name followed by dot and then parentheses to wrap an expression. As you can see, we still can access person one coming from the current scope, but also directly access teacher and director of school.
[01:24] In case you're wondering, if we add person one defined in set school it would by default use the one coming from school. By the way, local open also works for all sorts of data structures like lists and records.
[01:49] As an alternative you can use the keyword open to globally open a module and import its definition in the current scope. That said, I recommend to use it sparingly as it allows convenience at the cost of ease of reasoning.
[02:03] While open on the imports and modules definition, we can extend the module using the keyword include. We define a module game, which has a type states. In a module video game, we include this module.
[02:21] It basically statically copies over the definition of a module. When writing a function inside the module, we can use everything available that comes with game. In our case we can use the state one. So far so good.
[02:36] One thing that might be concerning though is that default every let binding and every type that we declare inside a module is exposed. To explicitly define what let bindings types and so on are accessible, we can define a module signature using the keyword module type. The signature name as well must start with a capital letter.
[02:57] In a signature block we can find the list of requirements that a module must satisfy in order for that module to match the signature. This can be very specific. For example, in our case the profession is very much set in stone, but for profession description we only declare that it accepts a profession and returns a string. In fact, you can't even write the implementation in that signature.
[03:22] To use a signature, we follow the same pattern as with any explicit type declaration. In case we don't match the full signature, the compiler will throw an error.
[03:33] Let's implement the profession description. We are good to go. Even though we have a signature, we can now extend our module. Here we add a let binding product and reuse it in our profession description.
[03:53] While the product is available inside the company's call, it's not available in the outside scope since it's not part of the signature. Now you should have a pretty good idea about modules. When should we use modules? We actually do all the time implicitly since every reason file automatically is a module.
[04:18] Previously, we defined a module math. Instead of doing so, we could have created a file math.re. To match the previous math module, we bring everything inside the module block into the file. In our case the binding is P and F.
[04:37] We didn't drive module math in here because this is implicit. If you would do so, we would actually create a module math inside the module math. In another module, for example main, we can use math. You ref print int of math.at new line and then print flowed of math.P.
[04:56] To actually try this out we edit these files to an existing progress group project. In the next lesson, we'll cover how to set up such a project. Let me add a native compiler would have worked as well since the module system behaves the same.
[05:11] When you compile our project and execute main, we can see both values have printed as expected. While it's not mandatory, it's recommended to start a file name with an uppercase character. In case you don't, Reason will automatically convert it.
[05:25] Foo.re would be available as module foo. How are signatures for such file modules? Had a file next to it with the same name, but instead of the file extension RE we use REI. We compile and we run main and everything still works as expected.
[05:52] If we remove P from our signature file, compilation will fail. As expected the issue here is that your signature file P isn't available anymore.