Tech News
← Back to articles

Basic dependency injection in OCaml with objects

read original related products more articles

In his article Why I chose OCaml as my primary language, my friend Xavier Van de Woestyne presents, in the section Dependency injection and inversion, two approaches to implementing dependency injection: one using user-defined effects and one using modules as first-class values. Even though I’m quite convinced that both approaches are legit, I find them sometimes a bit overkill and showing fairly obvious pitfalls when applied to real software. The goal of this article is therefore to briefly highlight the ergonomic weaknesses of both approaches, and then propose a new encoding of inversion and dependency injection that I find more comfortable (in many cases). In addition, this gives an example of using objects in OCaml, which are often overlooked, even though in my view OCaml’s object model is very interesting and offers a lot of practical convenience.

This approach is by no means novel and is largely the result of several experiments shared with Xavier Van de Woestyne during multiple pair-programming sessions. However, a precursor of this encoding can be found in the first version of YOCaml (using a Left Kan Extension/Freer Monad rather than a Reader). It's also interesting we can find a similar approach in the recent work around Kyo, an effect system for the Scala language, which uses a similar trick based on subtyping relationships to type an environment.

Why use dependency injection?

There are plenty of documents that describe (sometimes a bit aggressively) all the benefits of dependency injection, which are sometimes extended into fairly formalized software architectures (such as hexagonal architecture). For my part, I find that dependency injection makes unit testing a program trivial, which I think is reason enough to care about it (For example, the time-tracker I use at work, Kohai, uses Emacs as its interface and the file system as its database. Thanks to inversion and dependency injection, we were able to achieve high test coverage fairly easily).

Effect system and dependency injection

There are many different ways to describe an effect handler, so in my view it is difficult to give a precise definition of what an Effect system is. However, in our modern interpretation, the goal is more to suspend a computation so it can be interpreted by the runtime (the famous IO monad), and an Effect system is often described as a systematic way to separate the denotational description of a program, where propagated effects are operational “holes” that are given meaning via a handler, usually providing the ability to control the program’s execution flow (its continuation), unlocking the possibility to describe, for example, concurrent programs. In this article, I focus on dependency injection rather than on building an effect system because that would be very pretentious (my article does not address performance or runtime concerns). Similarly to how exception handling can be seen as a special case of effect propagation and interpretation (where the captured continuation is never resumed), I also see dependency injection as a special case, this time, where the continuation is always resumed. It’s quite amusing to see that dependency injection and exception capturing can be considered two special cases of effect abstraction, differing only in how the continuation is handled.

The Drawbacks of Modules and User-Defined Effects

As I mentioned in the introduction, I believe that both approaches proposed by Xavier are perfectly legitimate. However, after using both approaches in real-world software, I noticed several small annoyances that I will try to share with you.

Using modules

Using modules (through functors or by passing them as values) seems to be the ideal approach for this kind of task. However, the module language in OCaml has a different type system, in which type inference is severely limited. From my point of view, these limitations can lead to a lot of verbosity whenever I want to imagine that my dependencies come from multiple different sources. For example, consider these two signatures:

... continue reading