Reader Monad in Javascript

Exploring the Reader Monad in Javascript

August 25, 2019

The Reader Monad is probably one of the most commonly available monads available in the world of functional-programming. In this post we’ll be walking through an example that can benefit from using the Reader monad. We’ll be using Javascript and Monet.js to implement this.

Before Reader

Before we introduce Monet.js and Reader, let us build up an example. Assuming that we are functional programmers in a Javascript world, we tend to use functions and higher-order functions extensively. It is also not uncommon to find that our application require some configuration to be accepted at a much higher place than where it is required.

Let’s assume we are building an app that counts, but we want it to be able to count based on some configuration(predicate) higher up. For example, we could have a configuration that counts by 1, or by 2, 3, or primes only.

function* numberFactory(start, end) {
  for(let i = start; i <= end; i++) {
    yield i;
  }
}

function* numberFilter(start, end, predicate) { 
  const factory = numberFactory(start, end); // Generate all possibilities in range: start-end

  let result = factory.next(); 
  while(!result.done) {
    if (predicate(result.value)) {
      yield result.value;
    }
 	  result = factory.next();
  }
}

function collect(config) {
  const stream = numberFilter(config.start, config.end, config.predicate);
	const collectedValues = [];
  let result = stream.next();
  
  while (!result.done) {
    collectedValues.push(result.value);
    result = stream.next();
  }
  
  return collectedValues;
}

function app(config) {
  console.log(collect(config));
}

app({start: 1, end: 10, predicate: x => x % 2 == 0}) // Logs [2,4,6,8]

Granted this could have been implemented in a single loop, that goes from start to end and applies the predicate. However I wanted to represent the complexity a real world application might have and the difficulty that properly threading the configuration from the top level function to the bottom might present.

Before we dive into updating it to use Reader. I want to say that reader is all about making functions composeable and making configuration parameters (almost)invisible to functions that don’t need to deal with it.

After Reader

Below is the updated code that uses Reader. The biggest change is that function have become instaces of Reader. The function that is provided to the reader constructor is one that accepts a configuration parameter. So this way, any function that does not need the configuration can remain pure, and simply be treated as part of a pipeline and invoked in the map or flatMap.

const Reader = Monet.Reader; //monet.js

const numberFactory = Reader(function* ({start, end}) {
  for(let i = start; i <= end; i++) {
    yield i;
  }
});

const numberFilter = numberFactory.flatMap(stream => 
   Reader(function* ({ predicate }) {
    let result = stream.next(); 
    while(!result.done) {
      if (predicate(result.value)) {
        yield result.value;
      }
      result = stream.next();
    }
  })
);

const collect = numberFilter.map(stream => {
	const collectedValues = [];
  
  let result = stream.next();  
  while (!result.done) {
    collectedValues.push(result.value);
    result = stream.next();
  }
  
  return collectedValues;
});

function app(config) {
  console.log(collect.run(config))
}

app({start: 1, end: 10, predicate: x => x % 2 == 0}) // Logs [2,4,6,8]

Note in the above code how instead of calling methods like numberFactory and NumberFilter in side the methods, we have hoisted them out and mapped or flatMapped on their reader. This makes it much more obvious that methods like collect don’t rely on the top level configuration, even though functions downstream like numberFilter and numberFactory do. Furthermore, it also has the effect of clarifying what config settings are relied on by different functions. numberFactory relies on start and end but not predicate. Where are numberFilter only relies on predicate.

Another benefit of reader is how shuffling the relationships between these functions and even introducing new ones becomes trivial.

For example we could shuffle our definitions a bit and change make the overall structure of our program more explicit at a high-level inside of the app method.

This allows us to alter the behaviour our our application with more ease, without having to worry about rethreading parameters through existing methods.

const numberFactory = Reader(function* ({start, end}) { /* ...same... */ });
const numberFilter = stream => Reader(function* ({ predicate }) {
   /* ...same... */ 
})
;
const collect = stream => { /* ...same... */ };

function app(config) {
  numberFactory.map(collect).run(config); // Logs all numbers
  numberFactory.flatMap(numberFilter).map(collect).run(config); // Logs subset of number

  // Maybe add a new function that transforms the stream...
  numberFactory.flatMap(numberFilter).flatMap(numberTransform).map(collect).run(config);
}

Conclusion

The Reader Monad, and all monads are really about improving composability. Reader is a way to compose groups of functions that rely on a settings or configuration typically defined at a higher level. There is alot more that can be said about Monads, Reader, Monet.js, and functional-programming in this context, but I hope this helps shed some light on why/how Reader may be useful and powerful idea.

Additional Reading