Exploring Monads in JavaScript for File Handling

Written by maksimmuravev | Published 2023/04/18
Tech Story Tags: javascript-file-handling | monads | javascript | maybe-monad | either-monad | io-monad | programming | software-development

TLDRA monad is an abstraction over a chain of related computations. A container that takes the current state and a function that takes that state as an argument and returns the new state. Sounds hard? Not yet… 🙃 This container encapsulates the environment (context) for performing calculations, taking into account possible exceptions. The output is only the expected values. In general, just a “monoid in the category of endofunctors” (c). I hope now it’s clear 🧌via the TL;DR App

The widespread notion that JavaScript cannot interact with the file system is not entirely true. Rather, this interaction is significantly limited compared to server-side programming languages such as Node.js or PHP. Anyhow, JavaScript can both receive (read) and create (write) some file types and handle them successfully with native mechanisms.

One of the rare and complicated mechanism is… Monad.

So… What is a Monad?

Monad is not a fantastical creature or a rare gemstone. Instead, it's a design pattern from the functional programming (FP) paradigm.

A monad is an abstraction over a chain of related computations. A container that takes the current state and a function that takes that state as an argument and returns the new state. Sounds hard? Not yet… 🙃 This container encapsulates the environment (context) for performing calculations, taking into account possible exceptions. The output is only the expected values. In general, just a “monoid in the category of endofunctors” (c). I hope now it’s clear 🧌

Let's examine Monads together in this article. Our focus will not be on providing an extremely technical analysis but sooner on offering explanations through examples that demonstrate how Monads can extend JavaScript file handling to cosmic heights.

Why are Monads not popular in JavaScript?

Monads are not widely popular in JavaScript mostly because of three reasons:

  • Firstly, JavaScript historically was never intended to be a functional programming language, so it doesn't have natural support for Monads like other languages.
  • Secondly, Monads can be challenging to grasp and implement, especially for those unfamiliar with FP. Although, it would be a good challenge.
  • Finally, the adoption of Promises and Async/Await has surpassed the Monadic approach in the JavaScript community, rendering this article a form of "technological archaeology" for those who wish to distance themselves entirely from FP.

Although it may seem like Monads are thorny to wrap one’s head around, they can still be a practical instrument in your coding set - especially if you're feeling something warm to FP. In some examples, the benefits of using Monads even can outweigh its questionable learning curve.

If you're a JavaScript innovator/enthusiast seeking a unique way to manage files, Monads can prove quite useful. Say you must read a file, perform some processing, and write the output to another file. With the help of Either Monad to handle potential errors and the IO Monad to encapsulate side effects, you can effortlessly control the entire operation.

Say ciao to Promises.

And who knows, with Monads in your store of knowledge, you might even be able to impress (or disappoint, depending on your applied effort) your programming pals with your newfound FP prowess 😜.

I strongly suggest educating yourself with Monad before going down. This excellent guide provides a thorough explanation of its purpose and origins, making the rest of the article a breeze to read.

  1. First, let's create an Either Monad to handle errors:
// The Either monad is a type of container that can hold one of two possible values, typically referred to as the Left and the Right. 
// The Left and Right values are distinguished to provide additional context about the nature of the data being held.

class Either {
  
  // The static of() method creates a new instance of Right with the given value.
  static of(value) {
    return new Right(value);
  }

  // The static left() method creates a new instance of Left with the given value.
  static left(value) {
    return new Left(value);
  }
}

// The Left class extends the Either class and represents the Left side of the Either monad.
class Left extends Either {

  // The map() method returns this Left instance, as there is no value to transform.
  map() {
    return this;
  }

  // The chain() method returns this Left instance, as there is no value to pass to the given function.
  chain() {
    return this;
  }
}

// The Right class extends the Either class and represents the Right side of the Either monad.
class Right extends Either {

  // The map() method applies the given function to the value contained in this Right instance and returns a new Right instance with the transformed value.
  map(fn) {
    return Either.of(fn(this.value));
  }

  // The chain() method applies the given function to the value contained in this Right instance and returns the result.
  chain(fn) {
    return fn(this.value);
  }
}

  1. Next, let's create the IO Monad to encapsulate side effects:
// The IO monad is a container for side-effectful computations that can be safely composed and deferred. 
class IO {
  // The constructor takes an effectful computation as an argument and stores it as a property on the new IO instance.
  constructor(effect) {
    this.effect = effect;
  }

  // A static method used to create a new IO instance with a pure value.
  static of(value) {
    // Returns a new IO instance that wraps a function which returns the provided value.
    return new IO(() => value);
  }

  // A method used to apply a function to the result of the computation contained within the IO instance, while preserving the structure of the monad.
  map(fn) {
    // Returns a new IO instance that wraps a function which applies the provided function to the result of the computation.
    return new IO(() => fn(this.effect()));
  }

  // A method used to chain a function onto the computation contained within the IO instance.
  chain(fn) {
    // Returns a new IO instance created by mapping the provided function onto the computation, and then flattening the nested IO instances by joining them.
    return this.map(fn).join();
  }

  // A method used to join nested IO instances.
  join() {
    // Returns a new IO instance that wraps a function which retrieves the result of the computation contained within the nested IO instance.
    return new IO(() => this.effect().effect());
  }

  // A method used to run the computation contained within the IO instance.
  run() {
    // Returns the result of the computation.
    return this.effect();
  }
}

  1. Now, let's use these Monads to read a file, process its content, and write the result to another file:
// Require the built-in `fs` module.
const fs = require('fs');

// A function used to read a file, returning a new IO instance that either wraps the file contents, or an error object if the file cannot be read.
const readFile = (filename) =>
  new IO(() => {
    try {
      // Attempt to read the file and return the contents wrapped in a Right Either instance.
      return Either.of(fs.readFileSync(filename, 'utf8'));
    } catch (error) {
      // If the file cannot be read, return an error object wrapped in a Left Either instance.
      return Either.left(error);
    }
  });

// A function used to write to a file, returning a new IO instance that either wraps null, or an error object if the file cannot be written to.
const writeFile = (filename, content) =>
  new IO(() => {
    try {
      // Attempt to write to the file and return null wrapped in a Right Either instance.
      fs.writeFileSync(filename, content, 'utf8');
      return Either.of(null);
    } catch (error) {
      // If the file cannot be written to, return an error object wrapped in a Left Either instance.
      return Either.left(error);
    }
  });

// A function used to process file content by converting it to upper case.
const processContent = (content) => content.toUpperCase();

// A function that composes IO instances to read an input file, process its content, and write the result to an output file.
const main = (inputFile, outputFile) =>
  readFile(inputFile)
    .chain((content) => content.map(processContent))
    .chain((processedContent) => writeFile(outputFile, processedContent))
    .run();

// Execute the `main` function with input and output file names, and store the result in the `result` variable.
const result = main('input.txt', 'output.txt');

// Check the result of the computation and log either a success message or an error message.
if (result instanceof Left) {
  console.error('Error:', result.value);
} else {
  console.log('File processed successfully');
}

Let’s briefly go over what we just programmed.

The code presented implements two monads, Either and IO. Either monad operates as a container that can hold 1 of 2 values, namely Left and Right. The left class illustrates the Left side of the Either monad, whereas the Right class defines the Right side. The IO monad is a separate “combat unit“. It is a container for computations that may have side effects and can be safely deferred and composed. It is practical in encapsulating operations like I/O. Both monads have methods like map and chain, which enable value transformation and operation composition.

In ending, FP is a good shape of thinking, it is a very expressive and often succinct solution. And Monads within this, are a straightforward and robust design pattern used for function composition.

This article only gives a superficial, intuitive understanding of Monads. You can discover more by addressing to link I provided above and also going to Wikipedia, Python, and other resources.


Written by maksimmuravev | DevOps Engineer.
Published by HackerNoon on 2023/04/18