liveBook · Manning

Dependency inversion

As explained in Functional Design and Architecture: Examples in Haskell

The dependency inversion principle is a design principle requiring high-level policy to depend on abstractions rather than directly on low-level implementation details.

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

How Functional Design and Architecture: Examples in Haskell covers it

How deep it goes
Explains interfaces, injection, layered separation, and several FP approaches, while deferring their detailed implementations.
Examples
Uses greeting handles with alternate implementations and an astronomer application reporting meteors through a hidden channel.
What it assumes
Assumes familiarity with functional programming and IO while introducing coupling, interfaces, and dependency injection as design concerns.
What you can do after
Apply it via functional interfaces and injected implementations to decouple IO-bound business logic and substitute dependencies in tests.
How it connects
Connects dependency injection, Service Handle, ReaderT, free monads, GADTs, final tagless, layering, IO, and testability.

Also explained in

Compare how 6 books explain dependency inversion →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Design and Architecture: Examples in Haskell. The section links point to the parts of the book it draws on.