This article assumes you already know the following:
Monad is and how it works.compose functionYou can keep a log tracing the transformations that happened during the processing. It's particularly useful for debugging or monitoring.
The Logger acts like a Monad containing a List. Each transformation can emit a new log entry.
If one of the transformation throws an error for some reason, the error will be appended to the Logger and the processing will be aborted.
At the end of the processing, you can open the Monad to get the log entries (or ignore it depending on your use case).
CreateComposer and the compose functions
For the convenience, I created my own compose function, from the user point of view it's a regular compose function.
Under the hood it passes the Logger object and append new log entries to it. Basic error handling is also done there.
// Create a new logger (basically an array for now)
const logger = new Logger();
// This create a regular compose function and passes
// the logger internally
const compose = createComposer(logger);
The API for adding a new log entry implies to change JavaScript's String prototype which might not be the best idea. However, the API is very easy to use and the implementation is up to you.
function transform(value) {
return 'text'.describes(newValue);
}
In addition of returning your value it will return a function with the Logger as argument. As mentioned before the compose function will pass the Logger.
Logger object
The Logger is backed by a stack, it supports operations like push(Element) and map(Fn). It should also ideally allow append-only to it since the logs should be immutable.
class Logger {
constructor() {
this._stack = [];
this.push = this._stack.push;
this.map = this._stack.map;
}
}
I choose to implement it as an ES2015 Class to only export the methods I needed, but that's up to you.
const addOne = value =>
`add one to ${value}`.describes(
value + 1
);
const addTwo = value =>
`add two to ${value}`.describes(
value + 2
);
const divideByTwo = value =>
`divide ${value} by two`.describes(
value / 2
);
Define the bases:
const {createComposer, Logger} = require("monadic-logger");
// Create a new logger (basically an array for now)
const logger = new Logger();
// This create a regular compose function and passes
// the logger internally
const compose = createComposer(logger);
Describe our transformation pipeline:
const process = compose(
addOne,
addTwo,
divideByTwo
);
And get the result:
// 1 is the initial value
const res = process(1);
// As you would expect the value will be 2
console.log("value", res.value)
// And get the transformation trace
const trace = res.logger
.map((x, k) => `- step ${k + 1}: ${x}`)
.join('\n');
console.log(trace)
// - step 1: add one to 1
// - step 2: add two to 2
// - step 3: divide 4 by two
Imagine the step 2 failed, here is the result of the trace:
// - step 1: add one to 1
// - step 2: add two to 2
// - step 3: error: Value 4 can not be divided
noLog trick
The compose defined here will always append the new log entry, but imagine your transformation does not generate any logs. I defined noLog(Fn) function for this case.
Here is an example:
const minus3 = value => value - 3;
const process = compose(
addOne,
addTwo,
noLog(minus3)
);
This was heavily inspired by the work from Tony Morris which can be found here.