stateless-maybe-js
TypeScript icon, indicating that this package has built-in type declarations

2.2.3 • Public • Published

stateless-maybe-js

Build Status dist/maybe.min.js file size Greenkeeper badge Coverage Status TypeScript semantic-release

This code was ported to fpc as an es6 module. Check here fpc's Maybe docs.

Portable, lightweight, zero-dependency implementation of maybe monad — or option type — in vanilla JavaScript.

Why

There are a bunch of maybe-js libraries, all very similar to each other, so here's why I wrote this one.

  • Syntax

    Maybes are collections. I think a reasonable interface should be consistent with the collection interface of the host programming language.

    This choice enforces the least surprise principle: When you have to deal with a Maybe object you can use filter, map and forEach as you would with an array. Think of a Maybe as an array of at most one element.

  • Stateless

    I tried to follow functional paradigm as much as possible.

    Every Maybe object is an instance of Just or Nothing, which one is decided during creation. Once an object is created it will never be explicitly modified by the library.

    The wrapped value, anyway, is not side effect free: filter, map and forEach will apply a function on that value. Functions passed to those methods shouldn't modify the wrapped value (as far as possible) in order to keep everything stateless.

  • Portability

    This code could work nearly anywhere, natively:

    Here's my test environment for IE6.

    Here TypeScript type definitions.

    Besides you don't need a whole FP framework to simply create a Maybe, the minified version is lightweight.

  • Type consistency

    One thing I don't like in other approaches is that sometimes you can't tell the type of an expression at a glance.

    Every method here returns either a Maybe or a non-Maybe value, and I think this property helps a lot method chaining.

  • Information hiding

    When the wrapped value is publicly exposed the user code can still do something like

    if (maybe.value == null) {
      // ...
    }

    nullifying the purpose of the library.

    Here, on the other hand, the wrapped value is hidden and user code has to rely on public methods and properties.

Read more on design.

Installation

$ npm install stateless-maybe-js

For browser installation all you need is to include the script:

<script type="text/javascript" src="path/to/dist/maybe.min.js"></script>

or require in node:

const maybe = require('stateless-maybe-js');

Build

A Makefile will call yarn for you and then uglify-js to produce ./dist/maybe.min.js. Just point your console to the project path and run make.

How to create new Maybe

maybe(someValue) creates a new Maybe object wrapping someValue.

If someValue is null or undefined the result will be a Nothing instance, otherwise it'll be Just(someValue).

var m1 = maybe('hello, world');
var m2 = maybe(undefined);
var m3 = maybe(null);
 
m1.empty; // false
m2.empty; // true
m3.empty; // true

Maybe objects aren't nested by constructor function.

var m = maybe('hello, world');
 
// when maybe() receives a maybe monad
// it simply returns the maybe itself
=== maybe(m); // true

If the emptiness definition isn't trivial (i.e. null or undefined), you can use maybe.nothing and maybe.just().

function maybeYoungPeople (people, maxAge, atLeast) {
  var areYoung = people
    .reduce((acc, p) => acc && p.age <= maxAge, true);
 
  if (people.length >= atLeast && areYoung) {
    return maybe.just(people);
  } else {
    return maybe.nothing;
  }
}
 
var people = [ { age: 10 }, { age: 15 } ];
 
maybeYoungPeople(people, 16, 2).empty; // false
maybeYoungPeople(people, 14, 2).empty; // true
maybeYoungPeople(people, 16, 3).empty; // true

Note that maybe.just(), unlike maybe(), doesn't make any check. A Just instance is always created.

// `Maybe`s *can* contain null or undefined
var m1 = maybe.just(null);
 
m1.empty; // false
m1.get(); // null
 
var m2 = maybe('hello, world');
 
// `Maybe`s *can* be nested with `maybe.just()`
m2 !== maybe.just(m2);
m2 === maybe.just(m2).get();

In a nutshell with maybe.just() you are explicitly asking for a Just instance.

If you want to be sure the wrapped value isn't null or undefined, avoid maybe.just().

var m = notSure === 0
  ? maybe.nothing
  : maybe(notSure);

Check if a value is a Maybe

maybe.isInstance(null); // false
maybe.isInstance(maybe(null)); // true

Type specific constructors

All type-specific constructors also unbox their value before making checks, so 0 and Object(0) are treated identically.

maybe.string(value)

Checks if typeof value is string and it's not an empty string.

maybe.string(1) === maybe.nothing;
maybe.string('') === maybe.nothing;
 
maybe.string(Object('hello')).get() === 'hello';

maybe.number(value)

Returns maybe.just(value) if typeof value === 'number' and value isn't NaN.

// strings are *not* numbers
maybe.number('1') === maybe.nothing;
maybe.number(0/0) === maybe.nothing;
maybe.number(NaN) === maybe.nothing;
 
maybe.number(Object(1)).get() === 1;

maybe.object(value)

Checks if typeof value is object and it's not null.

maybe.object('') === maybe.nothing;
maybe.object(null) === maybe.nothing;
 
maybe.object(Object('hello')) === maybe.nothing;
maybe.object(Object(1)) === maybe.nothing;

Using Maybes

function maybeGetUser (id) {
  // ...
 
  return maybe(user);
}
 
// get user's date of birth or 'unknown'
// if user doesn't exist or user.dateOfBirth
// doesn't exist, is null or undefined
maybeGetUser(id)
  .map(user => user.dateOfBirth)
  .getOrElse('unknown');

You can use the maybe() function to wrap a lot of useful objects.

function maybeGetElementById (id) {
  return maybe(document.getElementById(id));
}
 
// remove an element if exist
maybeGetElementById('some-id')
  .forEach(element => element.remove());
 
// get header's height or 0
maybeGetElementById('header-id')
  .map(header => header.offsetHeight)
  .getOrElse(0);
 
// execute a function if an element exist
// or another function if it doesn't
maybeGetElementById('some-other-id')
  .forEach(e => console.log('element found!'))
  .orElse(() => console.log('element not found'));
 
// maybe.toString() returns an empty string
// on nothing
maybeGetElementById('some-node')
  .map(e => e.innerText)
  .toString();

Dealing with many Maybes seems hard at first and nesting functions might seem the only way to go. In this case filter could be a good option. For example we could write a function to update meta description only if the meta tag exists and the given description is a non-empty string:

// plain javascript
function updateMetaDescription (desc) {
  var metaDescription = document.getElementById('meta-description');
 
  if (metaDescription !== null && typeof desc === 'string' && desc !== '') {
    metaDescription.setAttribute('content', desc);
  }
}
 
// now nesting maybe.forEach
function updateMetaDescription (desc) {
  maybe(document.getElementById('meta-description'))
    .forEach(function (element) {
      maybe.string(desc).forEach(function () {
        // okay, this is worse
        element.setAttribute('content', desc);
      });
    });
}
 
// using maybe.filter
function updateMetaDescription (desc) {
  maybe(document.getElementById('meta-description'))
    .filter(() => maybe.string(desc).nonEmpty)
    .forEach(el => el.setAttribute('content', desc));
}

Other usage examples

Properties

maybe.empty

true if the maybe is nothing, false otherwise.

maybe.nonEmpty

Negation of maybe.empty.

Methods

maybe.filter(Function fn)

If the maybe is non-empty and fn(value) == false, returns nothing. Returns the maybe itself otherwise.

maybe.map(Function fn)

If the maybe is non-empty returns maybe(fn(value)). Returns nothing otherwise.

maybe.forEach(Function fn)

Applies the given function to the value if non-empty, does nothing otherwise. Always returns maybe itself.

maybe.get()

Returns wrapped value or throws an Error if the maybe is empty.

maybe.getOrElse(mixed orElse)

If the maybe is non-empty returns its value, returns orElse otherwise.

orElse can be:

  1. a function - which is called and its result returned if the maybe is empty.
  2. any other value - which is returned in case the maybe is empty.

maybe.getOrThrow(throwable e)

Returns the value of the maybe or throws e if the maybe is empty.

maybe.orElse(mixed orElse)

Acts like getOrElse, but returns an maybe instead of its value.

maybe.toString()

Returns the value casted to string if the maybe is non-empty, an empty string otherwise.

TypeScript

Since 2.1.0 TypeScript is also supported:

import * as maybe from 'stateless-maybe-js';
 
// Optionally import `Maybe` interface for type annotations
import { Maybe } from 'stateless-maybe-js';
 
// Instead of `maybe()` we can use `maybe.from()`.
let maybeStr: Maybe<string> = maybe.from('1');
 
// `filter` method won't change the type, so this assignment is valid
maybeStr = maybeStr.filter(str => str.trim() !== '');
 
function parseNum(str: string): Maybe<number> {
    const parsed = parseFloat(str);
 
    return isNaN(parsed) ? maybe.nothing : maybe.just(parsed);
}
 
let maybeNum: Maybe<number> = maybeStr.map(parseNum).forEach(console.log);

Package Sidebar

Install

npm i stateless-maybe-js

Weekly Downloads

1

Version

2.2.3

License

GPL-3.0

Unpacked Size

92.7 kB

Total Files

12

Last publish

Collaborators

  • emiliano.bovetti