Skip to content

Latest commit

 

History

History

maybe

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Maybe monad

Safely represent and manipulate a value which may be unavailable, possibly due to an error.

A Maybe monad is an immutable wrapper which allows you to defer handling of null/undefined values and errors until you want to capture the result of an operation. It's an alternative to frequent nullish checks and try/catch blocks.

Let's say you want to access a user object which is stored as a JSON string in session or local storage. The vanilla way to do so might look like the following.

function getUsername() {
  const json = sessionStorage.getItem('user')
    ?? localStorage.getItem('user');
  
  if (json == null) {
    return null;
  }

  // Might throw a JSON parsing error.
  const name = JSON.parse(json)?.name;

  if (typeof name !== 'string') {
    return null;
  }

  return name
}

// The above function might return a username,
// null, or throw an error. So you have to access
// it as follows.

let username: string | null;

try {
  username = getUsername();
} catch (error) {
  console.error(error);
}

if (username == null) {
  console.warn('no username');
} else {
  console.log('username = ' + username);
}

Using a Maybe monad makes this much cleaner, because each step in the operation can assume the input value is non-nullish (not null or undefined) and that there is no error. If there is an error, it's returned instead of thrown.

function getUsername(): Maybe<string> {
  return maybe(sessionStorage.getItem('user'))
    .else(() => localStorage.getItem('user'))
    .map((json) => JSON.parse(json)?.name)
    .filter((name) => typeof name === 'string');
}

const username = getUsername();

if (username.ok) {
  console.log('username = ' + username.value);
} else if (username.error != null) {
  console.error(username.error);
} else {
  console.warn('no username');
}

The Maybe monad has the following properties and methods.

  • ok - True if the monad is not empty (ie. has a non-nullish value).
  • empty - True if the monad is empty (ie. has no value).
  • value - Get the non-nullish value, or throw an error if the monad is empty.
  • error - Get the error (if any) that caused the monad to be empty.
  • map(next) - Get the next monad if the current monad is ok (not empty).
  • else(next) - Get the next monad if the current monad is empty, and does NOT have an error.
  • catch(next) - Get the next monad if the current monad has an error.
  • filter(predicate) - Get an empty monad if the current monad is ok (not empty), but does not match the predicate.
  • toArray() - Return an array containing the monad value if the monad is ok (not empty), or an empty array if the monad is empty.

In addition to creating a monad from an initial value (eg. maybe(value)), you can also create explicitly empty and error monads.

const empty = maybe.empty<Type>();
const error = maybe.error<Type>(new Error());

Wrapping a monad or returning a monad in a next callback, just returns the inner monad. So, monads can never be nested and the value of a monad is never another monad.

const a = maybe(1);
const b = maybe(a);
const c = maybe.empty().else(() => a);
a === b; // true
a === c; // true
a.value; // 1
b.value; // 1
c.value; // 1