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.