Functional Dependency Injection

Posted on April 8, 2022 by Joona

Let’s talk about functions! We all love functions, right? Well at least I do, I write tens of them every day! I have been a TypeScript programmer in my day job for the last 1,5 years, and would like to share some practices I have seen work, and others that.. well.. suck.

Today I want to talk about the problem of global configuration in a backend application.

Global configuration

Typically you see global configuration represented as an ordinary object. When I say global configuration this can mean constants like port, host and also system wide dependencies such as a database connection or even some state, such as the currently active user.

type Config = {
  // configuration constants
  port: number;
  host: string;
  // so called "dependencies"
  db: { getUserByUserName: (userName: string) => Promise<User> };
  logger: { info: (s: string) => void };
  // state variables
  currentUserName: Option<string>;
};

const config: Config = {
  port: process.env.PORT ?? 3000,
  host: process.env.HOST ?? "localhost",
  db: { getUserByUserName: () => Promise.resolve({ name: "some user" }) },
  logger: { info: console.log },
  currentUserName: O.some("some user"),
};

The most naive and unfortunately the most common way to consume global dependencies is something along the lines of the following code snippet.

const fetchUserNaive = (): Promise<Option<User>> => {
  const { db, logger, currentUserName } = config;
  return pipe(
    currentUserName,
    O.fold(
      () => Promise.resolve(O.none),
      (userName) => {
        logger.info(`fetching user with userName: ${userName}`);
        return db.getUserByUserName(userName).then(O.some);
      }
    )
  );
};

const hasAuthNaive = (): boolean => O.isSome(config.currentUserName);

Why is this so bad you might ask. Well:

Luckily improving from this is relatively simple. Let me introduce you to…

Function parameters

So the idea is to not depend on any values outside of our functions parameters, this forces us to list all of the dependencies the function has. When adapted to this style the previous code becomes:

const fetchUserParams = (cfg: Config): Promise<Option<User>> => {
  const { db, logger, currentUserName } = cfg;
  return pipe(
    currentUserName,
    O.fold(
      () => Promise.resolve(O.none),
      (userName) => {
        logger.info(`fetching user with userName: ${userName}`);
        return db.getUserByUserName(userName).then(O.some);
      }
    )
  );
};

const hasAuthParams: (currentUserName: Option<string>) => boolean = O.isSome;

Ahh, this is so much better. But why…, well let’s see what kind of improvements we get to the list of issues from before.

Is this dependency injection?

Pretty much, it is a really basic application of dependency injection or inversion of control for backend development. Especially in a functional language or style we don’t normally talk about dependency injection. But the idea is the same. In fact this kind of pattern in functional programming has been noticed a long time ago. We can abstract over passing configuration to our functions with the Reader data type.

const fetchUserReader: Reader<Config, Promise<Option<User>>> = pipe(
  R.ask<Config>(),
  R.map(({ currentUserName, db, logger }) =>
    pipe(
      currentUserName,
      O.fold(
        () => Promise.resolve(O.none),
        (userName) => {
          logger.info(`fetching user with userName: ${userName}`);
          return db.getUserByUserName(userName).then(O.some);
        }
      )
    )
  )
);

// This is a bit silly, I would probably leave this to the version without the reader
const hasAuthReader: Reader<Pick<Config, "currentUserName">, boolean> = pipe(
  R.ask<Pick<Config, "currentUserName">>(),
  R.map(({ currentUserName }) => O.isSome(currentUserName))
);

Without going to the detail you can think of the Reader<Config, string> as an abstraction for the following function (config: Config) => string. So it’s just a function in disquise!

This is a bit silly in this context, since the example is so simple but in principle here’s a couple of reasons you might consider the Reader data type.

Personally I propably wouldn’t introduce the Reader monad in a client project, since it can scare some people off and doesn’t bring enought benefits in the context of TypeScript to justify the costs (complexity, well again it’s in the eye of the reader).

Footnotes: