MyNixOS website logo
Description

Dependency injection for records-of-functions.

Put all your functions in the environment record! Let all your functions read from the environment record! No favorites!

dep-t

This package provides various helpers for the "record-of-functions" style of structuring Haskell applications.

A record that groups related functions is considered a component. Hypothetical example:

data Repository m = Repository
  { findById :: ResourceId -> m Resource,
    save :: Resource -> m ()
  } 

The record type is the component's "interface". A component's "implementation" is defined by a constructor function that returns a value of the record type.

When starting up, applications build a dependency injection environment which contains all the required components. And components read their own dependencies from the DI environment. The DI environment is akin to an ApplicationContext in object-oriented frameworks like Java Spring.

If components knew about the concrete DI environment, that would increase coupling. Everything would depend on everything else. To avoid that, we resort to Has-style typeclasses so that each constructor function knows only about the parts of the environment that it needs, and nothing more. Those Has-style classes can be tailor-made, but this package also provides a generic one.

Hypothetical example of constructor function:

makeRepository :: (Has Logger m deps, Has SomeOtherDep m deps) => deps -> Repository m

Very loosely speaking, Has-style constraints correspond to injected constructor arguments in object-oriented DI frameworks.

Module structure

  graph TD;
      Dep.Env-->Dep.Has;
      Dep.Env-->Dep.Phases;
      Dep.Constructor-->Dep.Phases;
      Control.Monad.Dep.Class-->Control.Monad.Reader;
      Control.Monad.Dep-->Control.Monad.Reader;
      Control.Monad.Dep-->Control.Monad.Dep.Class;
  • Dep.Has provides a generic Has typeclass for locating dependencies in an environment. Usually, component implementations import this module.
  • Dep.Env complements Dep.Has with helpers for building dependency injection environments. Usually, only the composition root of the application imports this module.
  • Dep.Phases provides a Phased typeclass for DI environments which go through a sequence of Applicative phases during construction. Also a special QualifiedDo notation for phases.
  • Dep.Constructor enables fixpoint-based dependency injection in Phased environments. See this thread in the Haskell Discourse for an example.
  • Control.Monad.Dep provides the DepT monad transformer, a variant of ReaderT. You either want to use this or Dep.Constructor in your composition root, but not both.
  • Control.Monad.Dep.Class is an extension of MonadReader, useful to program against both ReaderT and DepT.

Links

  • This library was extracted from my answer to this Stack Overflow question.

  • The implementation of mapDepT was teased out in this other SO question.

  • An SO answer about records-of-functions and the "veil of polymorphism".

  • The answers to this SO question gave me the idea for how to "instrument" monadic functions (although the original motive of the question was different).

  • I'm unsure of the relationship between DepT and the technique described in Adventures assembling records of capabilities which relies on having "open" and "closed" versions of the environment record, and getting the latter from the former by means of knot-tying.

    It seems that, with DepT, functions in the environment obtain their dependencies anew every time they are invoked. If we change a function in the environment record, all other functions which depend on it will be affected in subsequent invocations. I don't think this happens with "Adventures..." at least when changing a "closed", already assembled record.

    With DepT a function might use local if it knows enough about the environment. That doesn't seem very useful for program logic; if fact it sounds like a recipe for confusion. But it enables complex scenarios for which the dependency graph needs to change in the middle of a request.

    All in all, perhaps DepT will be overkill in a lot of cases, offering unneeded flexibility. Perhaps using fixEnv from Dep.Env will end up being simpler.

    Unlike in "Adventures..." the fixEnv method doesn't use an extensible record for the environment but, to keep things simple, a suitably parameterized conventional one.

  • Another exploration of dependency injection with ReaderT: ReaderT-OpenProduct-Environment.

  • The ReaderT design pattern.

    Your application code will, in general, live in ReaderT Env IO. Define it as type App = ReaderT Env IO if you wish, or use a newtype wrapper instead of ReaderT directly.

    Optional: instead of directly using the App datatype, write your functions in terms of mtl-style typeclasses like MonadReader and MonadIO

  • RIO is a featureful ReaderT-like / prelude replacement library which favors monomorphic environments.

  • The van Laarhoven Free Monad.

    Swierstra notes that by summing together functors representing primitive I/O actions and taking the free monad of that sum, we can produce values use multiple I/O feature sets. Values defined on a subset of features can be lifted into the free monad generated by the sum. The equivalent process can be performed with the van Laarhoven free monad by taking the product of records of the primitive operations. Values defined on a subset of features can be lifted by composing the van Laarhoven free monad with suitable projection functions that pick out the requisite primitive operations.

    Another post about the van Laarhoven Free Monad. Is it related to the final encoding of Free monads described here?

  • Interesting SO response (from 2009) about the benefits of autowiring in Spring. The record-of-functions approach in Haskell can't be said to provide true autowiring. You still need to assemble the record manually, and field names in the record play the part of Spring bean names.

    Right now I think the most important reason for using autowiring is that there's one less abstraction in your system to keep track of. The "bean name" is effectively gone. It turns out the bean name only exists because of xml. So a full layer of abstract indirections (where you would wire bean-name "foo" into bean "bar") is gone

  • registry is a package that implements an alternative approach to dependency injection, one different from the ReaderT-based one.

  • Printf("%s %s", dependency, injection). Commented on HN, Lobsters.

  • Dependency Injection Principles, Practices, and Patterns This is a good book on the general princples of DI.

  • A series of posts—by one of the authors of the DI book—about building a DI container.

  • Lessons learned while writing a Haskell application. This post recommends a "polymorphic record of functions" style, which fits the philosophy of this library.

  • One big disadvantage of the records-of-functions approach:

    representing effects as records of functions rather than typeclasses/fused effect invocations destroys inlining, so you’ll generate significantly worse Core if you use this on a hot path.

  • ReaderT pattern is just extensible effects.

Metadata

Version

0.6.8.0

Platforms (75)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64_be-none
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-darwin
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-darwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-windows