DEV Community

RawToast
RawToast

Posted on • Originally published at itazura.io

Dependency Injection in ReasonML using Modules

When I wrote my first project in ReasonML I just imported modules from anywhere as that just seemed to be the way things were done. This was fine 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
     }
   };
};
Enter fullscreen mode Exit fullscreen mode

The code should work fine, 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 an 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;
};
Enter fullscreen mode Exit fullscreen mode

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); 
};
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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 referred 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
     }
   };
};
Enter fullscreen mode Exit fullscreen mode

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
    }
  };
};

Enter fullscreen mode Exit fullscreen mode

Now, this module is easier to unit 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" };
};
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

I started using this technique partway 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.

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 built from a
module CreateGameLoop = (
  Pos: Types.Positions, 
  EL: EnemyLoop
) => { };

module CreateEnemyLoop = (
  Pos: Types.Positions, 
  Places: Types.Places, 
  World: World
) => { };

Enter fullscreen mode Exit fullscreen mode

Note that I chose to prefix the module names with CreateX, as they create a module that matches a module type name. I don't know what the preference is for calling these modules in Reason/OCaml, but it was a long-running debate in Java and C#...

So as demonstrated, you can use Reason's advanced module system for dependency injection. Whilst DI is often mentioned in object-oriented development – 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. In fact, DI is used in functional programming, often by using the Reader Monad or some construct derived from the Reader Monad such as RIO.

Based on a post initially published at itazura.io

Top comments (1)

Collapse
 
yawaramin profile image
Yawar Amin

Great article! Re:

I don't know what the preference is for calling these modules in Reason/OCaml,

I've typically seen them called Make, similarly to value-level make functions. And typically the signature is in the same module but called S. E.g.

// GameLoop.re

module type S = {
  ...
};

module Make = (Positions: Positions.S, EnemyLoop: EnemyLoop.S) => {
  ...
};

// Game.re

module Make = (GameLoop: GameLoop.S, ...) => {
  ...
};