The Environment Functions pattern utilizes first class effects to provide dependency inversion at a more granular level with less boilerplate than other patterns. The core idea is to define function types that are effect polymorphic, allowing implementations to utilize effects that aren't leaked into abstractions.

It’s often the case that dependency inversion patterns do a good job of hiding some implementation details, like what dependencies are required to instantiate some specific implementation or what backend/data retrieval process is being exactly utilized. However they are bad at hiding other implementation details, namely most effects. If your repository’s implementation has functions that require asynchronicity, error handling, or branching capability, those details will leak into the abstraction, otherwise the implementation’s function signature wouldn’t be valid.

The Environment Function Link to heading

This pattern differs from most other dependency inversion patterns in that we’re not defining an abstract type to hold a bunch of abstract functions, we just want one abstract function.

So while you might be used to something like:

trait MatchInfoSource:
  def fetchPlayerIds: List[String]
  def fetchMatchIds(playerId: String): List[String]
  def fetchMatch(matchId: String): String

We’d instead have each function in its own abstraction.

// apply is how we call an object as a function
trait FetchPlayerIds { def apply: List[String] }
trait FetchMatchIds { def apply(playerId: String): List[String] }
trait FetchMatch { def apply(matchId: String): String }

At first this probably sounds terrible, you’d have some crazy parameter explosion. Where previously you might have required 3 interfaces, you’d now need easily a dozen!

This is where another important component of the pattern comes in, the functions are actually required by the environment. What this means exactly is dependent on the language/framework. Sometimes what looks like just normally calling the function is actually requiring the function in the current environment and then calling it. Other times you’re building up sequence of data structures that will be later interpreted. For the purposes of this pattern, these two approaches are very similar.

Now what does “required by the environment” actually mean? It means rather than passing arguments down a chain of function calls, dependencies are resolved by some other mechanism. There are several things to consider here

  • Are dependencies resolved at compile time or runtime?
  • Does our method of dependency management compose with other effects?
  • How much boilerplate is there for resolving dependencies?

In FP Scala, we generally reach for the reader effect. In transformer code this means specifically using ReaderT, in kyo it means Env, and in mtl it means the Reader context bound. You can read my explanation on readers here. In these contexts, effects are first class and we can compose them seamlessly

def fetchAndSave(userId: Int) = for
  db: Database <- ask
  api: HttpClient <- ask

  user <- api.fetch(userId)
  _ <- db.save(user)
yield ()

Take this pseudocode:

function fetchAndSaveGame()
  ids = queryMatchIds()
  trackMatchIds(ids)

  playerId = fetchPlayerId()
  matchId = fetchMatchId(playerId)

  skipRestIfMatchTracked(matchId)
  trackMatchIds([matchId])

  match = fetchMatch(matchId)
  insertMatch(match)
  trackSuccess

Modern languages have generally adopted effect tracking, and while typed languages have much more robust effect tracking, even untyped languages like Python or Javascript have some level of effect invocation with keywords like async/throws/for. While arguably nothing is tracked because there are no real types (just runtime tags often called “types”), you still see the same boilerplate introduced.

Let’s say you’re inspired by the environment function pattern and want to go implement it in some language without support for first class effects. Notably, since it’s a real application, effects happen. Here’s an example of a set of effects you might implement: