cauldron
Dependency injection library
LTS Haskell 24.0: | 0.9.0.1 |
Stackage Nightly 2025-07-14: | 0.9.0.1 |
Latest on Hackage: | 0.9.0.1 |
cauldron-0.9.0.1@sha256:4f7e2d1d1c55ee4fea9120f1ace156546fd901764af863e70a9efc5aa06ef626,2557
Module documentation for 0.9.0.1
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. See here for a comparison of cauldron vs. manual in wiring a not-completely trivial app.
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.
Aggregate beans
More complex constructors can return—besides a “primary” bean as seen in the previous section—one or more secondary “aggregate” 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 final aggregated value of an aggregate bean by taking
the bean as a regular argument. Here, makeDebuggingServer
receives the
mappend
ed value of all the Inspector
s produced by other constructors (or
mempty
, if no constructor produces them):
makeDebuggingServer :: Inspector -> IO DebuggingServer
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 (besides the decorated bean itself), perform effects, and register aggregate beans:
makeServerDecorator :: Logger -> Server -> IO (Initializer,Server)
Example code
See this example application with dummy components.
For a slightly more realistic example, see here.
Similarities with the Java Spring framework IoC container
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 taking an “incomplete” set of constructors where not all constructor dependencies can be satisfied inside the set, and turning it into a single constructor which takes the missing dependencies as arguments, and can be made part of a wider set of constructors. The missing dependencies will then be read from that wider set.
-
injecting all the beans that implement a certain interface as a list roughly corresponds to a constructor that takes a aggregate 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
-
registry is a more mature and useable library for dependency injection in Haskell. See this explanatory video.
-
Dependency Injection Principles, Practices, and Patterns. This is a good book on the general principles of DI.
Acknowledgements
This package contains vendored code from Gabriella Gonzalez’s managed library.
Also vendored code from Andrey Mokhov’s
algebraic-graphs (most
of the cauldron:graph
library).
Changes
Revision history for cauldron
0.9.0.1
- compat with GHC 9.8.4.
0.9.0.0
- breaking change:
SomeRecipe
type hidden. - breaking change:
recipe
now produces aCauldron
, not aSomeRecipe
. - breaking change:
withRecipe
is nowlookup
and works onCauldron
s. - breaking change:
withRecipeCallstack
gone. Uselookup
. - breaking change:
IsList
instance forCauldron
has changed itsItem
type. - breaking change: the
bean
field ofRecipe
is now calledbare
. - new (|=|) and (䷱) operators.
0.8.1.0
-
Add
Cauldron.Managed.runManaged
. -
Monoid
andSemigroup
instances forManaged
, like the ones from the managed library. -
Export
Cauldron.Args.Args
fromCauldron
. This should make some IDE messages clearer.
0.8.0.0
-
doc and test changes.
-
re-export
arg
from Cauldron. -
breaking change:
MissingDependenciesError
now includes all the missing dependencies. -
breaking change:
DoubleDutyBeansError
is now a NonEmpty instead of a Map. -
Managed now has a MonadFail instance, like the one from the managed library.
0.7.0.0
- Remove dependency on algebraic-graphs, copying those parts of the code that we used.
- Remove
cookTree
andcookNonEmpty
. - Added
nest
. cook
is now “typed”: we pass the type of the bean we want to extract.RecipeError
->CookingError
.- Renamed
PrimaryBean
toFinishedBean
. - Renamed
SecondaryBean
toAggregateBean
. - Now the
Constructor
s don’t depend directly onSecondaryBean
/AggregateBean
. There is aPrimaryBean
/FinishedBean
that points to theSecondaryBean
/AggregateBean
, andConstructor
s depend on that. - Rename
collapseToPrimaryBeans
tocollapseBeans
. - Rename
removeSecondaryBeans
toremoveAggregates
.
0.6.1.0
ioEff
added toCauldron
.- New module
Cauldron.Builder
.
0.6.0.0
-
Remove sop-core dependency, incorporate just the needed functionality into the library.
Also make the internals of the library less dependent on n-ary tuples. Now they are more of an added layer for convenience.
-
The
cook
family of functions don’t return aDependencyGraph
anymore. Instead, the graph can be obtained at any moment usinggetDependencyGraph
, even for non-wireableCauldron
s. -
BoiledBeans
is now justBeans
and has its own module. -
A new
Cauldron.Args
module which defines theArgs
applicative. -
The way of creating
Constructor
s has been overhauled.Packer
andpack
are gone, along withvalue
,eff
and similar functions. The oldRegs
type is gone.To create
Constructor
s, now we should useval
andeff
along withwire
. -
The
Bean
record is now calledRecipe
. There’s also aSomeRecipe
that hides the bean type parameter. -
New
ToRecipe
typeclass that helps treating singleConstructor
s as recipes-without-decos. -
The
Decos
type is now just aSeq
of constructors of the same type. -
New
allowDepCycles
Fire
. -
Now
DependencyGraph
renders all the dependencies, even those that are ignored during plan construction to allow for dependency cycles. -
New
Monoid
instance forDependencyGraph
. -
BadBeans
is nowRecipeError
. It has now anException
instance and a pretty function. -
exportToDot
is nowwriteAsDot
and accepts aRecipeError
to highlight problematic nodes. -
Now
Constructor
s andRecipe
s keep track of theCallStack
of when they were created. This is used by errors to print the relevant code locations. Because now we have code locations,PathToCauldron
is no longer useful and has been removed.
0.4.0.0
exportToDot
takes a new parameter to configure how to print the steps. Before, only the TyCon was printed. Now, the full type is printed by default.
0.3.1.0
- Now the
MissingDependencies
only includes keys with actual missing dependencies.
0.3.0.0
- Add
cookNonEmpty
andcookTree
for cooking hierarchies of ‘Cauldron’s. - Rename
addLast
toaddOuter
andaddFirst
toaddInner
. - Add a copy of the
Managed
type from “managed”. - Change the nomenclature of the
pack-
related functions. - Add the
Packer
type. - Add
Fire
type to customize the handling of dependency cycles.
0.2.0.0
-
Decorators are no longer
Endo
s. They just take the decorated entity as a regular parameter. -
Remove the applicative wrappers.
-
Allow effectful constructors.
0.1.0.0 – YYYY-mm-dd
- First version. Released on an unsuspecting world.