Dependency inversion keeps business logic dependent on an interface rather than on the details of an external subsystem. The implementation is hidden behind that interface and selected elsewhere, so policy code can work without knowing whether it ultimately uses HTTP, TCP, or another channel. This reduces coupling, creates clear extension points, and is especially useful in IO-bound applications. section 13.2.1 section 13.2
Dependency injection is the practical mechanism: create an implementation at the application boundary and pass it to code that requires the capability. A service handle is a simple functional interface—a record of functions passed as an argument. Client code sees only those operations, while separate handle values provide different behavior. section 4.2.2 section 13.3
A dependency-inverted design separates responsibilities:
- Business logic declares the operations it needs, not the external subsystem it will use. section 13.2.1
- Application setup reads configuration and chooses an implementation before injecting it. section 13.2.1
- Implementations handle impure details while the business layer remains coupled to abstractions. section 13.2.1
- Tests substitute effects with mocks to isolate logic; easier substitution generally improves test quality. section 15.2.5
Inject a greetings capability
Define a GreetingsHandle with pure and impure greeting operations. helloUser accepts that handle, extracts the impure greeting operation, and calls it after reading a name. main can invoke helloUser with either handle1 or handle2, changing behavior without changing helloUser. The handle makes the dependency explicit and keeps the client independent of a particular greeting implementation. section 4.2.2
Functional interfaces vary in sophistication. Service Handle is among the simplest, while ReaderT stores a handle in a Reader environment. Free monads, GADTs, and final tagless/mtl offer other interface styles, with trade-offs in type-level complexity, effect restrictions, and test isolation. No single approach is presented as perfect. section 13.3 section 15.2.5
Table 15.1 Comparison of the testability of different approaches (view table figure)
| Dependency injection option |
Effect restrictions |
Bypassing restrictions |
Testability in general |
|
|---|---|---|---|---|
| Bare IO |
No DI |
No |
Easy |
Very low |
| ReaderT over IO |
Value based |
No |
Easy |
Low |
| Service handle within IO |
Value based |
No |
Easy |
Low |
| Final tagless/mtl |
Type based |
No |
Possible |
Low |
| Free monads |
Value based |
Yes |
Impossible |
Best |
Compare how dependency-injection choices affect effect restrictions, bypassing underlying interfaces, and general testability. section 15.2.5





