stateless-maybe-js
v2.2.3
Published
A stateless implementation of the maybe monad
Downloads
10
Maintainers
Readme
stateless-maybe-js
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
Maybe
s 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 usefilter
,map
andforEach
as you would with an array. Think of aMaybe
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 ofJust
orNothing
, 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
andforEach
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
m === 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 Maybe
s 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:
- a function - which is called and its result returned if the maybe is empty.
- 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);