
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;
}= factory.next();
result
}
}
function collect(config) {
const stream = numberFilter(config.start, config.end, config.predicate);
const collectedValues = [];
let result = stream.next();
while (!result.done) {
.push(result.value);
collectedValues= stream.next();
result
}
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;
}= stream.next();
result
}
});
)
const collect = numberFilter.map(stream => {
const collectedValues = [];
let result = stream.next();
while (!result.done) {
.push(result.value);
collectedValues= stream.next();
result
}
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 map
ped or
flatMap
ped 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) {
.map(collect).run(config); // Logs all numbers
numberFactory.flatMap(numberFilter).map(collect).run(config); // Logs subset of number
numberFactory
// Maybe add a new function that transforms the stream...
.flatMap(numberFilter).flatMap(numberTransform).map(collect).run(config);
numberFactory }
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.