Monadic logging

Requirements

This article assumes you already know the following:

Why?

You can keep a log tracing the transformations that happened during the processing. It's particularly useful for debugging or monitoring.

How does it work?

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.

JavaScript
        
// 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);
        
      

A log entry

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.

JavaScript
        
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.

The 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.

As a side note here is the actual implementation of my Logger
          
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.

The full example

Defining transformations

JavaScript
        
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
  );
        
      

Running the processing

Define the bases:

JavaScript
        
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:

JavaScript
        
const process = compose(
  addOne,
  addTwo,
  divideByTwo
);
        
      

And get the result:

JavaScript
        
// 1 is the initial value
const res = process(1);

// As you would expect the value will be 2
console.log("value", res.value)
        
      
JavaScript
        
// 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:

JavaScript
        
// - step 1: add one to 1
// - step 2: add two to 2
// - step 3: error: Value 4 can not be divided
        
      

The 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:

JavaScript
        
const minus3 = value => value - 3;

const process = compose(
  addOne,
  addTwo,
  noLog(minus3)
);
        
      

Inspiration

This was heavily inspired by the work from Tony Morris which can be found here.

Reach out

Say hello: [email protected].

Ping me on Twitter: @svensauleau.