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.