DEV Community

loading...

Dependency injection and Reader Monad

napicella profile image Nicola Apicella Updated on ・5 min read

If you are reading this article you are probably already familiar with dependency injection (DI for short) and its implications. In a nutshell,
DI suggests that object dependencies should be injected as parameters instead of hard coding them:

// Constructor creates the dependency
class A {
  private final B b; 
  A() {
    b = new B();
  }
}
// Versus the caller supplies the dependency of the class, 
// that's DI in action
class A {
  private final B b; 
  A(B b) {
    this.b = b;
  }
}

Enter fullscreen mode Exit fullscreen mode

Only recently I found out that it’s not the only way to dynamically inject dependencies. Another option comes from the functional world and it's called reader monad.
The reader monad let us bound values to functions without the need to pass the values around. One of the usage indeed is to postpone the injection of some data until we know the value it is going to assume.
Hopefully it is going to be more clear after reading the article.
In this article I won't (and can't) fully describe what a Monad is, but it's enough to know that a Monad is a parametrized type which wraps a value and allow to perform operations on it without unwrapping it. (Apologies to the Functional Programming Gods for the definition).
If you, like myself, need to associate the concept with something you already know, you might think about the Optional type in Java or the Either type of Scala:

    public static void main(String[] args) {
        System.out.println(friendsOf("Mickey"));
    }


    public static long friendsOf(String name) {
        // The optional might be empty or might contain the character we are 
        // looking for
        return getCharacterByName(name)
                .map((mickey) -> mickey.friends().count())
                .orElse(0L);
    }
Enter fullscreen mode Exit fullscreen mode

Warning: I am a newbie in functional programming! I am on quest to build a mental knob that let me switch between OOP and FP based on what I need.

Let’s come back to the main topic, dependency injection and reader monad.
Both can be used to inject dependencies thus both can be used to create more testable, predictable and reusable code. So which one should we use?

Quite a few times, after using DI as usual to supply dependencies to objects I realized there was something wrong with the API of those objects.
Generally speaking when using dependency injection it might happen that you need some object in a downstream call and this object need to be passed down until it reaches the downstream call.
This is pretty much unavoidable when you have a long lived object (say A) which depends on a short lived object (say B) which gets created based on some parameters we get at the root of the object graph. For example the long lived object might be something that we create only once when the application starts, while the short lived object gets created at runtime based on the user input.
sample-object-graph
When we have a long chain of calls between the point in which B gets created and the one in which B is used, then we are basically passing around B to methods which are not going to use it. That's interface pollution.

One solution could be to make the long lived object, a short lived one by reinstating it along with its dependency. This is not always desirable, because for example the object is expensive to create or is part of a object graph which gets created when the application starts (a graph of singleton, the good singleton not he the bad one :) ).

Example

This is a super simple example, just to show the point. All the code used in the article are in github.com/napicella/js-reader-monad.
Let's suppose we have a Controller which depends on some 'Service'.
The Service, in turns depends on another service. For sake of simplicity, let's imagine the following chain of calls:
Controller -> Service 1-> Service 2 -> Service 3.
Service 3 performs some computations using an Object which needs to be recreated based on the input received in the controller.
Here we have the sequence diagram:

sequence diagram

The controller creates 'obj' based on the parameters received and passes it down. Service 1 calls Service2.get and add '1' to the result. Similarly Service2 calls Service3.get and add '2' to the result. Finally Service3 uses 'obj' and return the result.

This is the sample code in javascript:

/**
 * create a simple object (short lived object in our example)
 */
function createObj(param1, param2) {
  return {
    val1 : param1,
    val2 : param2
  }
}

/**
 * Three long lived objects
 */
function Service1(otherService) {
  return {
    get : (obj) =>  {
      return otherService.get(obj) + 1;
    }
  }
}

function Service2(otherService) {
  return {
    get : (obj) =>  {
      return otherService.get(obj) + 2;
    }
  }
}

function Service3() {
  return {
    get : (obj) => doSomethingWith(obj)
  }
}

function doSomethingWith(someObj) {
  return someObj.val1 + someObj.val2;
}

/**
 * Simple Controller which uses a service to perform some computation
 */
function Controller(service) {
  return {
    someOp : function(param1, param2) {
      // Create an object based on some args recevied from the Controller and
      // pass the object to the service.
      // We could otherwise pass the params to service.
      //
      // Both solutions causes some sort of interface pollution because
      // the obj is only used in Service3
      var obj = createObj(param1, param2);
      return service.get(obj);
    }
  }
}

/**
 * Wire the object graph
 */
function Configuration() {
  return Controller(Service1(Service2(Service3())));
}


var controller = Configuration();
console.log(controller.someOp(10, 20));

Enter fullscreen mode Exit fullscreen mode

The code shows how the interface of the three services need to have obj as parameter even thought the parameter is only required for some computation in Service 3. The three services are long lived object created in a Configuration, while obj gets created in the controller based on the parameters received.

Another solution is to use the monad reader.
The idea is to abstract the computation in a monad, which can be used when we know the value that must be used as input to the computation.
In our case, this means that the computation done by the three services is going to be wrapped in a Reader.
Service3 returns a Reader , Service2 map a function on the value encapsulated in the Reader (value yet to be computed) and the same does Service1.

Sample code with the monad:

/**
 * Simple Reader Monad
 */
function reader(k) {
    return {
        run: function(e) {
            return k(e);
        },
        bind: function(f) {
            return reader(function(e) {
                return f(k(e)).run(e);
            });
        },
        map: function(f) {
            return reader(function(e) {
                return f(k(e));
            });
        }
    };
}

/**
 * create a simple object (short lived object in our example)
 */
function createObj(param1, param2) {
  return {
    val1 : param1,
    val2 : param2
  }
}

/**
 * Three long lived objects
 */
function Service1(otherService) {
  return {
    get : () =>  {
      return otherService.get().map((val) => val + 1);
    }
  }
}

function Service2(otherService) {
  return {
    get : () =>  {
      return otherService.get().map((val) => val + 2);
    }
  }
}

function Service3() {
  return {
    get : () => reader(doSomethingWith)
  }
}

function doSomethingWith(someObj) {
  return someObj.val1 + someObj.val2;
}

/**
 * Simple Controller which uses a service to perform some computation
 */
function Controller(service) {
  return {
    someOp : function(param1, param2) {
      // Create an object based on some args recevied from the Controller and
      // pass the object to the service.
      // The service returns a Reader, that we use to inject the object.
      // Thus we do not need to pass the object dowstream.
      var obj = createObj(param1, param2);
      return service.get().run(obj);
    }
  }
}

/**
 * Wire the object graph
 */
function Configuration() {
  return Controller(Service1(Service2(Service3())));
}


var controller = Configuration();
console.log(controller.someOp(10, 20));

Enter fullscreen mode Exit fullscreen mode

Here we go, no more obj in the API of the services.

Conclusions

Hope you find the article useful.
Any feedback would be greatly appreciated!


Notes
1 - reader in javascript

Discussion (6)

pic
Editor guide
Collapse
restuta profile image
Anton Vynogradenko

It would be interesting to see how this approach scales to multiple downstream dependencies.

Collapse
napicella profile image
Nicola Apicella Author

One way would be to replace multiple dependencies with a parameter object: refactoring.com/catalog/introduceP...

The object might be a domain object or a Tuple in case no domain object makes sense: tutorialspoint.com/scala/scala_tup...

Collapse
restuta profile image
Anton Vynogradenko

Yeah, that makes sense! Do yo have an idea how would that work with multiple dependencies that are needed for different objects at different levels in the object graph?

Usually, when, for example, writing tests, it’s required to “set up” multiple objects like that.

Thread Thread
napicella profile image
Nicola Apicella Author

Hi! I might have some, could you write up a short example to clarify?

Collapse
gurghet profile image
Andrea

What is the purpose of bind? How would you use it?

Collapse
napicella profile image
Nicola Apicella Author

Hi Andrea!

Bind is used when you you have to deal with functions that cross between the normal world and the Monadic world.
For example say, you have a function which returns a monad:

var db = (name) => reader(() => name);
Enter fullscreen mode Exit fullscreen mode

The function returns a reader, so bind can be used to compose by unwrapping the value (i.e. to avoid getting a monad of a monad)

var getPersonName = reader((x) => x.toUpperCase());
var res = getPersonName.bind(db)
res.run("Andrea")
// returns "ANDREA"
Enter fullscreen mode Exit fullscreen mode

Does that make sense?

The concept can be found in any functional languages and not only, although the name may change. Another common name for bind is flatMap.
If you know Java you have probably used flatMap to avoid getting an optional of an Optional.