Cauldron: a toy dependency injection framework

I’ve kept tinkering with this. I’ve tweaked the api and wrote a list of analogies with how DI works in the Java Spring Framework.

I’m not sure how to read your example, though. How might this actually look in a real scenario, e.g. to write a function that reads from a database and makes a network request?

As a slightly more realistic example, I’ve created a trivial web application with sqlite persistence. The wiring code looks like this:

cauldron :: Cauldron Managed
cauldron = do
  let liftConIO = hoistConstructor liftIO
  emptyCauldron
    & do
      let makeJsonConf = Bean.JsonConf.YamlFile.make do Bean.JsonConf.YamlFile.loadYamlSettings ["conf.yaml"] [] Bean.JsonConf.YamlFile.useEnv
      insert @(JsonConf IO) do makeBean do liftConIO do pack effect do makeJsonConf
    & insert @Logger do makeBean do pack effect do managed withStdOutLogger
    & insert @SqlitePoolConf do makeBean do liftConIO do pack effect do Bean.JsonConf.lookupSection @IO "sqlite"
    & insert @SqlitePool do makeBean do pack effect \conf -> managed do Bean.Sqlite.Pool.make conf
    & insert @(CurrentConnection M) do makeBean do pack value do Bean.Sqlite.CurrentConnection.Env.make id
    & insert @(CommentsRepository M) do makeBean do pack value do Comments.Repository.Sqlite.make
    & insert @(CommentsServer MH) do makeBean do pack value makeCommentsServer
    & insert @RunnerConf do makeBean do liftConIO do pack effect do Bean.JsonConf.lookupSection @IO "runner"
    & insert @Runner do makeBean do pack value makeRunner

Still kind of verbose I guess. Well, at least I got a nice dependency graph out of it:

Wiring errors are detected at runtime. For example, if I delete the insert @Logger line, the project will compile anyway, but when I try to run the server, I’m met with:

MissingDependencies [] (fromList [(Runner,fromList [Logger]),(CommentsServer (ReaderT Connection Handler),fromList [Logger]),(CommentsRepository (ReaderT Connection IO),fromList [Logger])])

Manual wiring or not, I like this way of structuring Haskell applications. Use records-of-functions to represent your components, make the components’ constructors take their dependencies as plain old function arguments, and wire the constructor together in a place near the “top” of the application, close to Main. The place where you wire things together is also good for adding logging, instrumentation and other decorations.