The I-Combinator in Practice

This is my second post on an on-going series on my experiments investigating the practical use of Combinators. As a recap, Combinators are functional patterns that glue (AKA compose) together functions. These patterns promote single responsibility and decoupling.

The I-Combinator is more commonly known as the Identity function. You've probably seen it before. The Identity function can be defined as such in C#:

Func<T, T> Identity<T>() => x => x;

It's a function that returns its own input. Simple as that, but how useful is that? Well, it turns out that in order to write code that is algebraic in nature, the structures should have algebraic properties. Algebraic code abide by the laws of mathematics making it easier to prove and are less error prone.

The Identity function is the Identity of a function and this allows it to be composable. If this trips you up, then these examples might make more sense:

  • 0 is the identity for sums
  • 1 is the identity for multiplication.

Reducing functions

When working with a list of integers, we can calculate the total as follows:

var numbers = {1, 2, 3, 4, 5};
var total = number.Aggregate(0, (a, b) => a + b);

Notice how 0 or the identity is passed into Aggregate (AKA reduce). Well, if we were to compose functions together, we could implement it as such:

var functions = { f, g, h, i }
var composed = functions.Aggregate(Identity, (a, b) => a(b));

For users of Redux, you'll notice that in most implementations of the Reducer, that if the Action is not recognized, then return the current state. This is an example of the Identity in action.

Conditional Pipeline Stages

Another use case is for functional pipelines. Given a pipeline, we can easily skip certain stages in the pipeline by returning the Identity function. In this case, the Identity function acts like a no-operation (noop), but allows the caller to still compose the pipeline without having to perform the conditional check.

public Func<T, T> DoSomething<T>(T input) {
  if (input.IsValid()) {
    return MyNewBehavior();
  } else {
    return Identity();

So the I-Combinator is pretty straightforward, but it's real power comes when combined with other Combinators. In fact, the most minimal turing complete language can be made with 3 Combinators and one of them is the I-Combinator.