When I wrote my first project Dokusho in ReasonML I just imported modules from anywhere as that just seemed to be the way things were done. This was ok when writing a simple React frontend, but when I later wrote the more complex backend for Bouken it didn't scale and quickly became messy and hard to test.

I was used to using dependency injection – often known as DI – as a pattern from writing Java and Scala, which is a pattern that enables inversion of control. Or more simply put, constructing an object or module from other modules/objects and using the functions provided.

Now in ReasonML  functors – functions from modules to modules – can be used to  implement dependency injection. Functors can be used for more advanced techniques, such as creating modules that parameterise other modules; however, this is a good introduction to the module system. When I first used this technique the language server failed to understand what I was doing! Thankfully, that issue been fixed!

Why would you want to use dependency injection? Well, for example, you could have a service that uses another module to make a call to an API:

module MyService = {
  let userAndComments = id => { 
     let user = UserApi.userById(id);
     switch(user) {
       | Some(user) => { 
       let comments = CommentsApi.fetchUserComments(id); 	
       Some({ user.name, comments });
       }
       | None => None
     }
   };
};

The code probably works, but are a few issues with this:

  • It can lead to a spiderweb of dependencies.
  • It can be hard to unit test, in this example you cannot test the userAndComments method without making a HTTP call. In order to test this module,  you would require either a mocked server or an integration test.

We can use dependency injection to control what the service receives in order to facilitate easier unit testing. So how would we go about that?

First, let define a type that we can inject. This is a simple type definition for logging output.

module type Logger = {
  let log: string => unit;
};
A simple logger module type

Then we can create a new module that uses the `Logger` we have defined. In OCaml and Reason a module that is built from other modules is known as a Functor.

module GreetingService = (LOG: Logger) => {
  let sayHello = name => LOG.log("Hello " ++ name); 
};
The power of CAPS-LOCK

Now we can implement a Logger and pass it into a GreetingService: creating a module from another module.

module ConsoleLogger = {
  let log = loggable => Js.Console.log(log);
};

module ConsoleGreeter = GreetingService(ConsoleLogger);

ConsoleGreeter.sayHello("World");

Note that we are free to choose how the Logger works – it only has to match the type signature. For example, you could implement an ErrorLogger instead:

module ErrorLogger = {
  let log = loggable => Js.Console.error(log);
};

Effectively we have taken away control from GreetingService to Logger. Now GreetingService describes what it wants to do, but doesn't actually implement how to do it. This is often reffered to as inversion of control.

When writing systems in a modern object-oriented style using programming languages such as Java or Kotlin, this technique is heavily used. Often you will read that you should "prefer composition over inheritance" and DI is used to alongside composition to create cleaner more testable code.

This pattern can be used in ReasonML (and OCaml), by using functors for dependency injection. Lets go back to the original example to demonstrate this:

Updating the Original Service

Remember this? It's the MyService module from the first example.

module MyService = {
  let userAndComments = id => { 
     let user = UserApi.userById(id);
     switch(user) {
       | Some(user) => { 
       let comments = CommentsApi.fetchUserComments(id); 	
       Some({ user.name, comments });
       }
       | None => None
     }
   };
};

We can turn MyService into a functor and construct an instance by passing module types for UserApi and CommentsApi.

module MyService = (Users: UserApi, Comments: CommentsApi) => {
  let userAndComments = id => { 
    let user = Users.userById(id);
    switch(user) {
      | Some(user) => { 
      let comments = Comments.fetchUserComments(id); 	
      Some({ user.name, comments });
      }
     | None => None
    }
  };
};
In OCaml a module that is built from other modules is known as a Functor

Now this module is easier to test. You can pass in stubbed versions of the modules in order to test the flow of the userAndComments function.

module StubUserApi = {
  let userById = id => switch(id) {
    | 1 => Some({ name: "Test User"})
    | _ => None
  };
};

module StubCommentsApi = {
  let fetchUserComments = id => 
    { title: "Test Comment", message: "Hello" };
};

Now it's easy to test the service using these simple stubbed modules:

module UnderTest = MyService(StubUserApi, StubCommentsApi);

let result = UnderTest.userAndComments(1); // Some({ ...
let anotherResult = Undertest.userAndComment(2); // None

I started using this part way through building Bouken. As previously stated, I initially imported modules from anywhere as that seemed to be the way things were done and worked for my frontend logic. Shortly afterwards, the game loop became unwieldy and needed breaking up.

RawToast/bouken
React based ASCII Rogue. Contribute to RawToast/bouken development by creating an account on GitHub.
There's a link on the repository page.

Over time the more complex modules were broken down into smaller modules for easier testing and code management. This eventually turns into a dependency tree of module types. For example, the main game builder is defined as follows:

module CreateGame: (
  Types.GameLoop, 
  Types.World, 
  Types.WorldBuilder
) => (Types.Game) = (
    GL: Types.GameLoop, 
    W: Types.World, 
    WB: Types.WorldBuilder
  ) => { };
  
// Which is build outf
module CreateGameLoop = (
  Pos: Types.Positions, 
  EL: EnemyLoop
) => { };

module CreateEnemyLoop = (
  Pos: Types.Positions, 
  Places: Types.Places, 
  World: World
) => { };
Why "createX"? Module types were defined elsewhere such as `module type GameLoop = { }`

So as demonstrated, you can use Reason's advanced module system for dependency injection – you do not need to use objects! Instead, use modules for organising your code and leave objects alone until you absolutely require open types or inheritance.