Exploring ReasonML
Please support this book: buy it or donate
(Ad, please don’t block.)

11 Basic modules

In this chapter, we explore how modules work in ReasonML.

11.1 Installing the demo repository

The demo repository for this chapter is available on GitHub: reasonml-demo-modules. To install it, download it and:

cd reasonml-demo-modules/
npm install

That’s all you need to do – no global installs necessary.

11.2 Your first ReasonML program

This is where your first ReasonML program is located:

reasonml-demo-modules/
    src/
        HelloWorld.re

In ReasonML, each file whose name has the extension is .re is a module. The names of modules start with capital letters and are camel-cased. File names define the names of their modules, so they follow the same rules.

Programs are just modules that you run from a command line.

HelloWorld.re looks as follows:

/* HelloWorld.re */

let () = {
  print_string("Hello world!");
  print_newline()
};

This code may look a bit weird, so let me explain: We are executing the two lines inside the curly braces and assigning their result to the pattern (). That is, no new variables are created, but the pattern ensures that the result is (). The type of (), unit, is similar to void in C-style languages.

Note that we are not defining a function, we are immediately executing print_string() and print_newline().

To compile this code, you have two options (look at package.json for more scripts to run):

Therefore, our next step is (run in a separate terminal window or execute the last step in the background):

cd reasonml-demo-modules/
npm run watch

Sitting next to HelloWorld.re, there is now a file HelloWorld.bs.js. You can run this file as follows.

cd reasonml-demo-modules/
node src/HelloWorld.bs.js

11.2.1 Other versions of HelloWorld.re

As an alternative to our approach (which is a common OCaml convention), we could have also simply put the two lines into the global scope:

/* HelloWorld.re */

print_string("Hello world!");
print_newline();

And we could have defined a function main() that we then call:

/* HelloWorld.re */

let main = () => {
  print_string("Hello world!");
  print_newline()
};
main();

11.3 Two simple modules

Let’s continue with a module MathTools.re that is used by another module, Main.re:

reasonml-demo-modules/
    src/
        Main.re
        MathTools.re

Module MathTools looks like this:

/* MathTools.re */

let times = (x, y) => x * y;
let square = (x) => times(x, x);

Module Main looks like this:

/* Main.re */

let () = {
  print_string("Result: ");
  print_int(MathTools.square(3));
  print_newline()
};

As you can see, in ReasonML, you can use modules by simply mentioning their names. They are found anywhere within the current project.

11.3.1 Submodules

You can also nest modules. So this works, too:

/* Main.re */

module MathTools = {
  let times = (x, y) => x * y;
  let square = (x) => times(x, x);
};

let () = {
  print_string("Result: ");
  print_int(MathTools.square(3));
  print_newline()
};

Externally, you can access MathTools via Main.MathTools.

Let’s nest further:

/* Main.re */

module Math = {
  module Tools = {
    let times = (x, y) => x * y;
    let square = (x) => times(x, x);
  };
};

let () = {
  print_string("Result: ");
  print_int(Math.Tools.square(3));
  print_newline()
};

11.4 Controlling how values are exported from modules

By default, every module, type and value of a module is exported. If you want to hide some of these exports, you must use interfaces. Additionally, interfaces support abstract types (whose internals are hidden).

11.4.1 Interface files

You can control how much you export via so-called interfaces. For a module defined by a file Foo.re, you put the interface in a file Foo.rei. For example:

/* MathTools.rei */

let times: (int, int) => int;
let square: (int) => int;

If, e.g., you omit times from the interface file, it won’t be exported.

The interface of a module is also called its signature.

If an interface file exists, then docblock comments must be put there. Otherwise, you put them into the .re file.

Thankfully, we don’t have to write interfaces by hand, we can generate them from modules. How is described in the BuckleScript documentation. For MathTools.rei, I did it via:

bsc -bs-re-out lib/bs/src/MathTools-ReasonmlDemoModules.cmi

11.4.2 Defining interfaces for submodules

Let’s assume, MathTools doesn’t reside in its own file, but exists as a submodule:

module MathTools = {
  let times = (x, y) => x * y;
  let square = (x) => times(x, x);
};

How do we define an interface for this module? We have two options.

First, we can define and name an interface via module type:

module type MathToolsInterface = {
  let times: (int, int) => int;
  let square: (int) => int;
};

That interface becomes the type of module MathTools:

module MathTools: MathToolsInterface = {
  ···
};

Second, we can also inline the interface:

module MathTools: {
  let times: (int, int) => int;
  let square: (int) => int;
} = {
  ···
};

11.4.3 Abstract types: hiding internals

You can use interfaces to hide the details of types. Let’s start with a module Log.re that lets you put strings “into” logs. It implements logs via strings and completely exposes this implementation detail by using strings directly:

/* Log.re */

let make = () => "";
let logStr = (str: string, log: string) => log ++ str ++ "\n";

let print = (log: string) => print_string(log);

From this code, it isn’t clear that make() and logStr() actually return logs.

This is how you use Log. Note how convenient the pipe operator (|>) is in this case:

/* LogMain.re */

let () = Log.make()
  |> Log.logStr("Hello")
  |> Log.logStr("everyone")
  |> Log.print;

/* Output:
Hello
everyone
*/

The first step in improving Log is by introducing a type for logs. The convention, borrowed from OCaml, is to use the name t for the main type supported by a module. For example: Bytes.t

/* Log.re */

type t = string; /* A */

let make = (): t => "";
let logStr = (str: string, log: t): t => log ++ str ++ "\n";

let print = (log: t) => print_string(log);

In line A we have defined t to be simply an alias for strings. Aliases are convenient in that you can start simple and add more features later. However, the alias forces us to annotate the results of make() and logStr() (which would otherwise have the return type string).

The full interface file looks as follows.

/* Log.rei */

type t = string; /* A */
let make: (unit) => t;
let logStr: (string, t) => t;
let print: (t) => unit;

We can replace line A with the following code and t becomes abstract – its details are hidden. That means that we can easily change our minds in the future and, e.g., implement it via an array.

type t;

Conveniently, we don’t have to change LogMain.re, it still works with the new module.

11.5 Importing values from modules

There are several ways in which you can import values from modules.

11.5.1 Importing via qualified names

We have already seen that you can automatically import a value exported by a module if you qualify the value’s name with the module’s name. For example, in the following code we import make, logStr and print from module Log:

let () = Log.make()
  |> Log.logStr("Hello")
  |> Log.logStr("everyone")
  |> Log.print;

11.5.2 Opening modules globally

You can omit the qualifier “Log.” if you open Log “globally” (within the scope of the current module):

open Log;

let () = make()
  |> logStr("Hello")
  |> logStr("everyone")
  |> print;

To avoid name clashes, this operation is not used very often. Most modules, such as List, are used via qualifications: List.length(), List.map(), etc.

Global opening can also be used to opt into different implementations for standard modules. For example, module Foo might have a submodule List. Then open Foo; will override the standard List module.

11.5.3 Opening modules locally

We can minimize the risk of name clashes, while still getting the convenience of an open module, by opening Log locally. We do that by prefixing a parenthesized expression with Log. (i.e., we are qualifying that expression). For example:

let () = Log.(
  make()
    |> logStr("Hello")
    |> logStr("everyone")
    |> print
);
11.5.3.1 Redefining operators

Conveniently, operators are also just functions in ReasonML. That enables us to temporarily override built-in operators. For example, we may not like having to use operators with dots for floating point math:

let dist = (x, y) =>
  sqrt((x *. x) +. (y *. y));

Then we can override the nicer int operators via a module FloatOps:

module FloatOps = {
  let (+) = (+.);
  let (*) = (*.);
};
let dist = (x, y) =>
  FloatOps.(
    sqrt((x * x) + (y * y))
  );

Whether or not you actually should do this in production code is debatable.

11.5.4 Including modules

Another way of importing a module is to include it. Then all of its exports are added to the exports of the current module. This is similar to inheritance between classes in object-oriented programming.

In the following example, module LogWithDate is an extension of module Log. It has the new function logStrWithDate(), in addition to all functions of Log.

/* LogWithDateMain.re */

module LogWithDate = {
  include Log;
  let logStrWithDate = (str: string, log: t) => {
    let dateStr = Js.Date.toISOString(Js.Date.make());
    logStr("[" ++ dateStr ++ "] " ++ str, log);
  };
};
let () = LogWithDate.(
  make()
    |> logStrWithDate("Hello")
    |> logStrWithDate("everyone")
    |> print
);

Js.Date comes from BuckleScript’s standard library and is not explained here.

You can include as many modules as you want, not just one.

11.5.5 Including interfaces

Interfaces are included as follows (InterfaceB extends InterfaceA):

module type InterfaceA = {
  ···
};
module type InterfaceB = {
  include InterfaceA;
  ···
}

Similarly to modules, you can include more than one interface.

Let’s create an interface for module LogWithDate. Alas, we can’t include the interface of module Log by name, because it doesn’t have one. We can, however, refer to it indirectly, via its module (line A):

module type LogWithDateInterface = {
  include (module type of Log); /* A */
  let logStrWithDate: (t, t) => t;
};
module LogWithDate: LogWithDateInterface = {
  include Log;
  ···
};

11.5.6 Renaming imports

You can’t really rename imports, but you can alias them.

This is how you alias modules:

module L = List;

This is how you alias values inside modules:

let m = List.map;

11.6 Namespacing modules

In large projects, ReasonML’s way of identifying modules can become problematic. Since it has a single global module namespace, there can easily be name clashes. Say, two modules called Util in different directories.

One technique is to use namespace modules. Take, for example, the following project:

proj/
    foo/
        NamespaceA.re
        NamespaceA_Misc.re
        NamespaceA_Util.re
    bar/
        baz/
            NamespaceB.re
            NamespaceB_Extra.re
            NamespaceB_Tools.re
            NamespaceB_Util.re

There are two modules Util in this project whose names are only distinct because they were prefixed with NamespaceA_ and NamespaceB_, respectively:

proj/foo/NamespaceA_Util.re
proj/bar/baz/NamespaceB_Util.re

To make naming less unwieldy, there is one namespace module per namespace. The first one looks like this:

/* NamespaceA.re */
module Misc = NamespaceA_Misc;
module Util = NamespaceA_Util;

NamespaceA is used as follows:

/* Program.re */

open NamespaceA;

let x = Util.func();

The global open lets us use Util without a prefix.

There are two more use cases for this technique:

The latter technique is used by BuckleScript for Js.Date, Js.Promise, etc., in file js.ml (which is in OCaml syntax):

···
module Date = Js_date
···
module Promise = Js_promise
···
module Console = Js_console

11.6.1 Namespace modules in OCaml

Namespace modules are used extensively in OCaml at Jane Street. They call them packed modules, but I prefer the name namespace modules, because it doesn’t clash with the npm term package.

Source of this section: “Better namespaces through module aliases” by Yaron Minsky for Jane Street Tech Blog.

11.7 Exploring the standard library

There are two big caveats attached to ReasonML’s standard library:

11.7.1 API docs

ReasonML’s standard library is split: most of the core ReasonML API works on both native and JavaScript (via BuckleScript). If you compile to JavaScript, you need to use BuckleScript’s API in two cases:

This is the documentation for the two APIs:

11.7.2 Module Pervasives

Module Pervasives contains the core standard library and is always automatically opened for each module. It contains functionality such as the operators ==, +, |> and functions such as print_string() and string_of_int().

If something in this module is ever overridden, you can still access it explicitly via, e.g., Pervasives.(+).

If there is a file Pervasives.re in your project, it overrides the built-in module and is opened instead.

11.7.3 Standard functions with labeled parameters

The following modules exist in two versions: an older one, where functions have only positional parameters and a newer one, where functions also have labeled parameters.

As an example, consider:

List.map: ('a => 'b, list('a)) => list('b)
ListLabels.map: (~f: 'a => 'b, list('a)) => list('b)

Two more modules provide labeled functions:

11.8 Installing libraries

For now, JavaScript is the preferred platform for ReasonML. Therefore, the preferred way of installing libraries is via npm. This works as follows. As an example, assume we want to install the BuckleScript bindings for Jest (which include Jest itself). The relevant npm package is called bs-jest.

First, we need to install the package. Inside package.json, you have:

{
  "dependencies": {
    "bs-jest": "^0.1.5"
  },
  ···
}

Second, we need to add the package to bsconfig.json:

{
  "bs-dependencies": [
    "bs-jest"
  ],
  ···
}

Afterwards, we can use module Jest with Jest.describe() etc.

More information on installing libraries: