/cauldron

Dabbling yet again with dependency injection

Primary LanguageHaskellOtherNOASSERTION

cauldron

Double, double toil and trouble;

Fire burn and caldron bubble.

Fillet of a fenny snake,

In the caldron boil and bake;

cauldron is a library for performing dependency injection. It's an alternative to manually wiring the constructors for the components ("beans") of your application.

It expects the bean constructors to conform to a certain shape.

cauldron should be used at the composition root. Bean constructors shouldn't be aware that cauldron exists, or depend on its types.

cauldron relies on dynamic typing and finds wiring errors at runtime, not compilation time.

Why you should(n't) use this library

To be honest, you probably shouldn't use this library. I have noticed that using cauldron is actually more verbose that manually doing the wiring yourself. Perhaps it would start to pay for complex beans with many dependencies, but I'm not sure.

Another possible objection to this library is that wiring errors are detected at runtime. I don't find that to be a problem though: the wiring happens at the very beginning of the application, and it's easy to write an unit test for it.

On the plus side, this library lets you render the graph of dependencies between beans, something which is difficult to do with naive manual wiring.

Another advantage is that you can easily modify an existing web of dependencies, be it by inserting a new bean, overriding another, or adding a decorator.

The expected shape of constructors

cauldron expects "bean" constructors to have a shape like:

makeServer :: Logger -> Repository -> Server

Where Logger, Repository and Server are records-of-functions. Server is the component produced by this constructor, and it has Logger and Repository as dependencies.

Sometimes constructors are effectful because they must perform some initialization (for example allocating some IORef for the internal Server state). In that case the shape of the constructor becomes something like:

makeServer :: Logger -> Repository -> IO Server

or even, for constructors which want to ensure that resources are deallocated after we are finished using the bean:

makeServer :: Logger -> Repository -> Managed Server

Having more than one constructor for the same bean type is disallowed. The wiring is type-directed, so there can't be any ambiguity about which bean constructor to use.

Registering secondary beans

More complex constructors can return—besides a "primary" bean as seen in the previous section—one or more "secondary" beans. For example:

makeServer :: Logger -> Repository -> (Initializer, Inspector, Server)

or

makeServer :: Logger -> Repository -> IO (Initializer, Inspector, Server)

These secondary outputs of a constructor, like Initializer and Inspector, must have Monoid instances. Unlike with the "primary" bean the constructor produces, they can be produced by more than one constructor. Their values will be aggregated across all the constructors that produce them.

Constructors can depend on the aggregated value of a secondary bean by taking the bean as a regular argument. Here, makeDebuggingServer receives the mappended value of all the Inspectors produced by other constructors (or mempty, if no constructor produces them):

makeDebuggingServer :: Inspector -> IO DebuggingServer

Constructors that produce secondary beans require a bit more work from the cauldron user. He must tell the library which is the primary bean and which are the secondary ones, because it's not detected automatically.

Decorators

Decorators are like normal constructors, but they're used to modify a primary bean, instead of producing it. Because of that, they usually take the bean they decorate as an argument:

makeServerDecorator :: Server -> Server

Like normal constructors, decorators can have their own dependencies (other than the decorated bean), perform effects, and register secondary beans:

makeServerDecorator :: Logger -> Server -> IO (Initializer,Server)

Example code

See this example application with dummy components.

Some features of this library have loose analogues in how Java Spring handles dependency injection (although of course Spring has many more features).

First, a big difference: there's no analogue here of annotations, or classpath scanning. Beans and decorators must be explicitly registered.

  • Java POJOs are Haskell records-of-functions, where the functions will usually be closures which encapsulate access to some shared internal state (state like configuration values, or mutable references). Functions that return records-of-functions correspond to POJO constructors.

  • @PostConstruct roughly corresponds to effectful constructors.

    Although I expect effectful constructors to be used comparatively more in this library than in Spring, because here they're required to initialize mutable references used by the beans.

  • decorated self-invocations correspond to constructors that depend on the same bean that they produce.

    Note that this is different from decorators that depend on the bean they modify. The constructor will receive the fully decorated bean "from the future" (with the possibility of infinite loops if it makes use of it too eagerly). In contrast, a decorator will receive either the bare "undecorated" bean, or the in-construction result of applying the decorators that come earlier in the decorator sequence.

  • context hierachies correspond to distributing the constructors into various sets organized in parent-child relationships, so that constructors in a child can see the beans of the parent, but not vice-versa.

  • injecting all the beans that implement a certain interface as a list roughly corresponds to a constructor that takes a monoidally aggregated "secondary bean" as an argument.

Some features I'm not yet sure how to mimic:

  • bean scopes, like request scope. This Stack Overflow post gives some information about how they are implemented in Spring.

    The SO post explains that in Spring the injection of request scoped beans into long-lived beans involves thread-local variables. I explored such a technique for Cauldron here.

See also