DIVEX

Dependency Injection can be Functional

TL;DR dependency injection can be functional if we compose potentially-pure functions and only inject impure functions at the last moment.

Dependency injection means that units of behavior (e.g. classes and functions) declare their dependencies as parameters (e.g. function parameters or class constructor parameters) and that some other entity provides values for such dependencies at composition time (e.g. in the Composition Root at application startup).

Functional programming is about programming with pure functions (among other things). I.e., functions that are deterministic and side-effect-free.

The following C# function is potentially-pure:

public static void SayHi(Action<string> write, string name)
{
    write("Hello, " + name + "!");
}

It doesn’t invoke impure methods in its body, but it invokes the write function which is passed to it as a parameter. The purity of SayHi is dependent on the purity of the function passed as a value for write.

The following are examples of usages of SayHi that produce (1) an impure function (2) a pure function (3) a potentially-pure function

The following function is impure:

public static void SayHiUsingConsole(string name)
{
    SayHi(Console.WriteLine, name);
}

It is impure because it uses Console.WriteLine. It passes Console.WriteLine as a value for the write parameter of SayHi.

The following is a pure function:

public static string SayHiAndReturnResult(string name)
{
    var sb = new StringBuilder();

    SayHi(s => sb.AppendLine(s), name);

    return sb.ToString();
}

This method is pure because its output is completely determined by its input (the name parameter) and it doesn’t cause any state mutation outside the scope of this method. It basically informs SayHi to append into a StringBuilder when it wants to write the Hi message and then returns the contents of the StringBuilder.

The following is a potentially-pure function:

public static void SayHiToMultiplePeople(Action<string> write, string[] names)
{
    foreach (var name in names)
    {
        SayHi(write, name);
    }
}

SayHiToMultiplePeople has a write parameter. It passes its value to SayHi. So now the purity of SayHiToMultiplePeople is dependent on this write parameter.

We can keep creating potentially-pure functions out of other potentially-pure ones. Here is an example:

public static void StoreTheFactThatWeSaidHi(
  Action<string, string> appendToFile, string[] names)
{
    appendToFile("c:\\logfile.txt", "We said Hi to " +
                 string.Join(", ", names) +
                 Environment.NewLine);
}

public static void SayHiToMultiplePeopleAndStoreTheFact(
    Action<string> write,
    Action<string, string> appendToFile,
    string[] names)
{
    SayHiToMultiplePeople(write, names);

    StoreTheFactThatWeSaidHi(appendToFile, names);
} 

Both StoreTheFactThatWeSaidHi and SayHiToMultiplePeopleAndStoreTheFact are potentially-pure.

But this is not dependency injection. For example, SayHiToMultiplePeopleAndStoreTheFact has a concrete dependency on SayHiToMultiplePeople and StoreTheFactThatWeSaidHi. Also, SayHiToMultiplePeople has a concrete dependency on SayHi.

On the other hand, this is dependency injection:

public static void SayHi(Action<string> write, string name)
{
    write("Hello, " + name + "!");
}

public static void SayHiToMultiplePeople(Action<string> sayHi, string[] names)
{
    foreach (var name in names)
    {
        sayHi(name);
    }
}

public static void StoreTheFactThatWeSaidHi(
  Action<string, string> appendToFile, string[] names)
{
    appendToFile("c:\\logfile.txt", "We said Hi to " +
                 string.Join(", ", names) +
                 Environment.NewLine);
}

public static void SayHiToMultiplePeopleAndStoreTheFact(
    Action<string[]> sayHiToMultiplePeople,
    Action<string[]> storeTheFactThatWeSaidHi,
    string[] names)
{
    sayHiToMultiplePeople(names);

    storeTheFactThatWeSaidHi(names);
}

public static void Compose()
{
    Action<string> sayHi = name => SayHi(Console.WriteLine, name);

    Action<string[]> sayHiToMultiplePeople =
      names => SayHiToMultiplePeople(sayHi, names);

    Action<string[]> storeTheFactThatWeSaidHi =
      names => StoreTheFactThatWeSaidHi(File.AppendAllText, names);

    Action<string[]> sayHiToMultiplePeopleAndStoreTheFact = names =>
        SayHiToMultiplePeopleAndStoreTheFact(
      	    sayHiToMultiplePeople, storeTheFactThatWeSaidHi, names);

    sayHiToMultiplePeopleAndStoreTheFact(new[] { "John", "Jane" });
}

SayHiToMultiplePeopleAndStoreTheFact no longer has a concrete dependency on SayHiToMultiplePeople or StoreTheFactThatWeSaidHi. Instead, it takes function parameters that it calls (sayHiToMultiplePeople and storeTheFactThatWeSaidHi). Also, SayHiToMultiplePeople no longer has a concrete dependency on SayHi.

The Compose function composes these functions. Although the defined four functions are potentially-pure, once we start doing dependency injection, the composed functions become impure very quickly.

For example, In the first line of the Compose method, sayHi (the local variable) is an impure function because it passes Console.WriteLine to the SayHi function. sayHiToMultiplePeople (the local variable) is also impure because it passes sayHi to SayHiToMultiplePeople. storeTheFactThatWeSaidHi and sayHiToMultiplePeopleAndStoreTheFact (the local variables) are also impure for the same reasons.

Consider this updated Compose2 method:

public static void Compose2()
{
    void SayHiToMultiplePeopleAndStoreTheFactComposed(
      string[] names, Action<string> write, Action<string, string> appendToFile)
    {
        SayHiToMultiplePeopleAndStoreTheFact(
            names1 => SayHiToMultiplePeople(name => SayHi(write, name), names1),
            names1 => StoreTheFactThatWeSaidHi(appendToFile, names1),
            names);
    }

    SayHiToMultiplePeopleAndStoreTheFactComposed(
      new[] { "John", "Jane" }, Console.WriteLine, File.AppendAllText);
} 

In this new Compose2 method, SayHiToMultiplePeopleAndStoreTheFactComposed is a local function that is potentially-pure. It is potentially pure because the purity of it depends on the purity of the write and appendToFile functions passed to it. Inside this local function, we do dependency injection. That is, the SayHiToMultiplePeopleAndStoreTheFactComposed function is composed of the four defined functions, but it is done in a way to keep SayHiToMultiplePeopleAndStoreTheFactComposed potentially-pure.

What is the value of this?

Basically, if we keep all functions potentially-pure and compose them into a single potentially-pure function, then we can delay the injection of impure functions (like Console.WriteLine and File.AppendAllText) to the last possible moment. This makes it easy to do unit testing. We can make SayHiToMultiplePeopleAndStoreTheFactComposed a normal static method and then use it in a unit test and pass fakes for the write and appendToFile parameters. If we know that SayHiToMultiplePeopleAndStoreTheFactComposed is potentially-pure then we know that the only way this function can communicate with anything is through its parameters (e.g. write and appendToFile).

Now, the SayHiToMultiplePeopleAndStoreTheFactComposed function looks ugly and it will look uglier if we had 30 functions instead of 4.

Consider this Compose3 method written in a language called Composition Language 1:

public static void Compose3()
{
    var sayHiToMultiplePeople = SayHiToMultiplePeople[SayHi];

    var sayHiToMultiplePeopleAndStoreTheFact =
        SayHiToMultiplePeopleAndStoreTheFact[
            sayHiToMultiplePeople, StoreTheFactThatWeSaidHi];

    sayHiToMultiplePeopleAndStoreTheFact(
      Console.WriteLine, File.AppendAllText, new[] { "John", "Jane" });
}

Better, right?

In this language, we are allowed to inject SayHi into SayHiToMultiplePeople without:

  1. Specifying a value for the write parameter of SayHi.
  2. Specifying a value for the names parameter of SayHiToMultiplePeople.

Similarly, when we injected sayHiToMultiplePeople and StoreTheFactThatWeSaidHi into SayHiToMultiplePeopleAndStoreTheFact, we were able to do so without:

  1. Specifying a value for the write parameter of sayHiToMultiplePeople.
  2. Specifying a value for the appendToFile parameter of StoreTheFactThatWeSaidHi.

Here is the signature of sayHiToMultiplePeople (the local variable):

You can run the code above in the browser using this link:

https://divex.dev/try-in-the-browser-cscompose/?sample=ComposePotentiallyPureFunctions

Composition Language 1 is part of DIVEX. For an introduction to DIVEX, see https://divex.dev/knowledge-base/main/an-introduction-to-divex/. For more information about Composition Language 1, see https://divex.dev/knowledge-base/concepts/composition-language-1/

With DIVEX and Composition Language 1, it is easy to compose potentially-pure functions.

How good are potentially-pure functions?

I think that potentially-pure functions are almost as good as pure functions, maybe even better. In both cases we know that nothing impure is happening inside the functions behind our backs.

A pure function has no parameters that are functions meaning that any output from the function is returned when the function returns. Also, a pure function cannot request new inputs (via function parameters) after it begins execution. This is both good and bad.

It is good because it is simpler. A pure function takes same values and produces some values. That’s easier to test. It is bad because in many scenarios, we would like a function to take new inputs while it is executing and return outputs before it is done executing.

Potentially-pure functions don’t have such restrictions. And they still bring many of the benefits of pure functions like knowing that nothing impure is happening behind our backs.

1 response to Dependency Injection can be Functional

Comments are closed.