1) Record of functions. Probably the most common, and pretty analogous to calling a constructor in OO. If you've heard "objects are a poor man's closures, and vice-versa", that's what this is referring to. You build a closure with as many dependencies as you can pass in at startup time (fields in OO), then later you can just call one of the functions inside it, with the remaining parameters (method call in OO).
2) final tagless. After you're comfortable with an Optional<Value> that might return a Value, and Future<Value> that will return a Value later, you can venture further down that path and work with a Writer<Value> which will return a value and do some logging, or a Reader<Env, Value> which will return a value while having read-access to some global config. But what if you want many of these at once? You end up with a ReaderT Env (Writer Value) or some nonsense. So instead you can write your code in terms of m<Value>, and then constrain m appropriately. A function which can access Git, do logging, and run commands on the CLI might have type:
(Git m, Logging m, Cli m) => m Value.
But that function does not know what m is (so therefore does not depend on it) You might like to have different m's for unit tests, integration tests, a live test environment and a live prod environment, for instance.3) Free Monad & interpreter pattern. A nifty way to build a DSL describing the computations you'd like to do, and then you can write different interpreters to execute those computations in different ways. For instance, you could write a network/consensus algorithm, and have one interpreter execute the whole thing in memory with pure functions, for rapid development, debugging and iterating, and another interpreter be the real networked code. It's fun, but someone wrote an article about how Free Monads are just a worse version of final tagless, so I haven't used this in a while.
class Git m where
clone :: Url -> m GitRepo
currentHead :: GitRepo -> m CommitHash
and Logging might be something like class Logging m where
log :: String -> m ()
which is very similar to defining data GitDict m = MkGitDict {
clone :: Url -> m GitRepo,
currentHead :: GitRepo -> m CommitHash
}
data LoggingDict m = MkLoggingDict {
log :: String -> m ()
}
class Git m where gitDict :: GitDict m
class Logging m where loggingDict :: LoggingDict m
So the "record of functions" style and the "final tagless" style are equivalent, except that the former passes operations manually and the latter passes operations implicitly. The former can be considered more cumbersome, but is more flexible.If you're interested in how my effect system Bluefin does this, you can have a look at https://hackage.haskell.org/package/bluefin-0.0.4.2/docs/Blu...
I guess part of the confusion came from how dependency injection is a form of inversion of control… the words are all very similar to dependency inversion.
With DI, you stop your business logic from knowing about your Postgres database.
With DInjF, not only does business logic still know about Postres, but now it knows about Spring too! (The upside is that it takes fewer lines of code to get from 0 to spaghetti)
Dependency injection and the Dependency inversion principle are not one and the same.
The principle makes a claim, that inversion is a good onto itself.
Injection is a tool not a claim.
Incidentally, one thing it glosses over is the creation of the components, which may prevent completely inverting the dependency.