@7urtle/lambda
Managing side effects with monads in JavaScript

Instead of explaining the laws governing monads and explaining that monad is just a monoid in the category of endofunctors, we will focus on the practical application of monads with @7urtle/lambda.

To clarify things you can understand monads as wrappers that aim to simplify the dealing with side effects, errors, inputs, outputs, and asynchronous code. You can also see them as containers around values or functions.

As you probably know, functional programming is very particular about keeping your functions pure dependent only on their inputs and not causing any external effects. But real life usually requires your code to read data from somewhere and then save or display some results. Among side effects, you can count output into console, reading files, updating database, editing DOM, saving data, and so on. For all of these you can use monads.

In @7urtle/lambda we have Maybe, Either, Case, SyncEffect, and AsyncEffect. All of these are functors, monads, and applicatives at the same time because they implement methods map, flatMap, and ap.

map

Every functor has the method map. map allows you to apply a function on a value that the functor wraps around safely. It's all easier to understand on an example:

1
import {deepInspect, map} from '@7urtle/lambda';
2
3
// we define our own functor Functor
4
const Functor = {
5
of: value => ({
6
value: value,
7
inspect: () => `Functor(${deepInspect(value)})`,
8
map: fn => Functor.of(fn(value))
9
})
10
};
11
12
const add1 = a => a + 1;
13
14
// we wrap Functor around the value 1
15
const myFunctor = Functor.of(1); // Functor(1)
16
myFunctor.inspect(); // => 'Functor(1)'
17
18
// we map the function add1 on the value inside the functor
19
const myFunctorPlus1 = myFunctor.map(add1); // Functor(2)
20
21
myFunctorPlus1.value; // => 2
22
myFunctor.map(a => a + 1).value; // => 2
23
myFunctor.value; // => 1, no mutation
24
25
// you can also apply function map on a functor with the same result
26
myFunctorPlus1again = map(add1)(myFunctor);
27
myFunctorPlus1again.value === myFunctorPlus1.value; // => true
28
29
// it is the same map that we use for arrays
30
map(add1)([1, 2]); // => [2, 3]

Now if you understand the example and how the function map works, you now understand the basics of functors in functional programming. Specialized monads then add a special magic to these and you can learn about that later when those are discussed.

flatMap

If you had a function that returns a functor and you mapped the function on a functor, you would end up with a functor inside of a functor. Every monad has the method flatMap. flatMap allows you to map a function the same as map but it expects that the mapped function returns the same type of monad as is the type of the monad that we are mapping it on. It deals with it by flattening the result into a simple value inside of a monad instead of a value in a monad in a monad. Here is an example:

1
import {deepInspect, flatMap} from '@7urtle/lambda';
2
3
// we define our own monad Monad
4
const Monad = {
5
of: value => ({
6
value: value,
7
inspect: () => `Monad(${deepInspect(value)})`,
8
map: fn => Monad.of(fn(value)),
9
flatMap: fn => fn(value)
10
})
11
};
12
13
// we wrap Monad around the value 1
14
const myMonad = Monad.of(1); // Monad(1)
15
16
const add1Monad = a => Monad.of(a + 1);
17
18
// we map the function add1 on the value inside the monad
19
myMonad.map(add1Monad); // Monad(Monad(2));
20
const myMonadPlus1 = myMonad.flatMap(add1Monad); // Monad(2)
21
22
myMonadPlus1.value; // => 2
23
myMonad.value; // => 1, no mutation
24
25
// you can also apply function flatMap on a monad with the same result
26
myMonadPlus1again = map(add1Monad)(myMonad);
27
myMonadPlus1again === myMonadPlus1; // => true

Now if you understand the example and how the function flatMap works, you now understand the basics of monads in functional programming.

ap

Because in functional programming we also love partial application, we have applicatives and their method ap. ap allows you to map curried functions that require multiple values using map and apply those values using ap. More in the following example:

1
import {deepInspect} from '@7urtle/lambda';
2
3
// we define our own applicative Applicative
4
const Applicative = {
5
of: value => ({
6
value: value,
7
inspect: () => `Applicative(${deepInspect(value)})`,
8
map: fn => Applicative.of(fn(value)),
9
flatMap: fn => fn(value),
10
ap: f => f.map(value)
11
})
12
};
13
14
// we wrap Applicative around the value 1
15
const myApplicative = Applicative.of(1); // Applicative(1)
16
17
const add = a => b => a + b;
18
19
// we map the function add on the applicative
20
const myApplicativePartial = myApplicative.map(add); // Applicative(1 + b);
21
const myApplicativePlus1 = myApplicativePartial.ap(Applicative.of(1)); // Applicative(2)
22
const myApplicativePlus3 = myApplicativePartial.ap(Applicative.of(3)); // Applicative(4)
23
24
myApplicativePlus1.value; // => 2
25
myApplicative.value; // => 1, no mutation
26
27
// you can also apply function flatMap on a monad with the same result
28
myMonadPlus1again = map(add1Monad)(myMonad);
29
myMonadPlus1again === myMonadPlus1; // => true

Now if you understand the example and how the function ap works, you now understand the basics of applicatives in functional programming.

@7urtle/lambda also gives you functions liftA2 and liftA3 to simplify writing functions that work with applicatives. It takes a regular function that expects simple values and turns it into a function that supports applicatives:

1
import {liftA2} from '@7urtle/lambda';
2
3
const add = a => b => a + b;
4
5
const addAp = liftA2(add);
6
7
myResultApplicative = addAp(Applicative.of(2))(Applicative.of(3));
8
myResultApplicative.value; // => 5

Composing with monads

You are still working with functional programming paradigm and your code should be based on composing pure functions. Consider monads as new language types or containers. Your functions should use monads as their outputs and you should prefer to create functions that partially apply map or flatMap rather than call these functions directly on a monad. Here are some examples:

1
import {Maybe, map, flatMap, compose} from '@7urtle/lambda';
2
3
// the correct way
4
const getElement = selector => Maybe.of(document.querySelector(selector));
5
const getClientHeight = map(a => a.clientHeight); // partial application of map
6
const getDOMHeight = compose(getClientHeight, getElement);
7
8
getDOMHeight('#someElement');
9
10
// the working but not recommended ways
11
12
// breaks single responsibility and impacts reusability
13
const getDOMHeight2 = selector => Maybe.of(document.querySelector(selector)).map(a => a.clientHeight);
14
15
// doesn't use functions, instead uses monads as statement or OOP objects
16
Maybe
17
.of(document.querySelector('#someElement'))
18
.map(a => a.clientHeight);

This approach will help you build more reusable code following general programming best practices.

Now all that remains is the magic of specific monads in @7urtle/lambda.

Maybe there is a value

Maybe is the most simple monad to learn. Maybe expects to maybe get a value as its input. It evaluates whether the value is Nothing (empty, null, or undefined) or Just. In other languages, Maybe monad can also be called Option monad or Nullable monad.

1
import {Maybe, maybe, isNothing, sJust} from '@7urtle/lambda';
2
3
Maybe.of('some value'); // Just('some value');
4
Maybe.of(''); // Nothing
5
Maybe.of([]); // Nothing
6
Maybe.of({}); // Nothing
7
Maybe.of(null); // Nothing
8
Maybe.of(undefined); // Nothing
9
10
Maybe.of(5).isJust(); // => true
11
Maybe.of(5).value; // => 5
12
Maybe.of([]).isNothing(); // => true
13
Maybe.of([]).value; // => []
14
15
// function maybe is there to help you process the result
16
const testMaybe =
17
maybe
18
('Maybe is Nothing')
19
(value => 'Maybe is: ' + value);
20
21
testMaybe(Maybe.of([])); // => 'Maybe is Nothing'
22
testMaybe(Maybe.of('something')); // => 'Maybe is: something'
23
24
// @7urtle/lambda also provides independent isNothing and isJust functions
25
isNothing(''); // => true
26
isJust(''); // => false

Maybe allows you to safely map over its value as the mapped function is applied only if the value is Just.

1
import {Maybe} from '@7urtle/lambda';
2
3
Maybe.of(5).map(a => a + 5); // Just(10)
4
Maybe.of(undefined).map(a => a + 5); // Nothing
5
6
Maybe.of(5).flatMap(a => Maybe.of(a + 5)); // Just(10)
7
Maybe.of(undefined).flatMap(a => Maybe.of(a + 5)); // Nothing
8
9
Maybe.of(5).map(a => b => a + b).ap(Maybe.of(5)); // Just(10)
10
Maybe.of(undefined).map(a => b => a + b).ap(Maybe.of(5)); // Nothing

Your own function should return Maybe in cases that you are unsure that there is an output usually in when you are working with side effects. For example you can use it like this when working with DOM:

1
import {Maybe, map, compose} from '@7urtle/lambda';
2
3
// returns DOM element or undefined if the element #imayexist doesn't exist
4
document.querySelector('#imayxist');
5
6
// returns position from top or throws and error if the element doesn't exist
7
document.querySelector('#imayxist').offsetTop;
8
9
// Let's try it with Maybe without any worry about throwing errors
10
const selectMaybeDOM = selector => Maybe.of(document.querySelector(selector));
11
const getPositionFromTop = element => element.offsetTop;
12
const getMaybePositionFromTopDOM = compose(map(getPositionFromTop), selectMaybeDOM);
13
14
// or just one line (which goes against single responsibility principle)
15
const getMaybePositionFromTopDOM2 = selector => Maybe.of(document.querySelector(selector)).map(a => a.offsetTop));
16
17
const MaybeResult = getMaybePositionFromTopDOM('#imayexist');
18
MaybeResult.isNothing() ? 'Element does not exist' : 'I have result ' + MaybeResult.value;

Either it is Success or Failure

Either is another simple monad to learn. Either represents either Success or Failure of execution to allow you to manage error states in your code. For your convenience there is also a function try to help with try/catchfor functions in nodejs that may throw an error.

1
import {Either, either} from '@7urtle/lambda';
2
3
Either.of('ok'); // Success('ok')
4
Either.of('ok').value; // => 'ok'
5
6
Either.Success('ok'); // Success('ok')
7
Either.Failure('error'); // Failure('error')
8
9
const iThrowError = () => {
10
throw new Error('I am an error.');
11
};
12
13
Either.try(() => 'ok'); // Success('ok')
14
Either.try(iThrowError); // Failure('I am an error.')
15
16
// function either is there to help you process the result
17
const testEither =
18
either
19
(error => 'There is an error: ' + error)
20
(value => 'Your result is: ' + value);
21
22
testEither(Either.Success('ok')); // => 'Your result is: ok'
23
testEither(Either.Failure('error')); // => 'There is an error: error'

Either allows you to safely map over its value as the mapped function is applied only if the value is Success.

1
import {Either} from '@7urtle/lambda';
2
3
Either.of(5).map(a => a + 5); // Success(10)
4
Either.Failure(5).map(a => a + 5); // Failure(5)
5
6
Either.of(5).flatMap(a => Either.of(a + 5)); // Success(10)
7
Either.Failure('error').flatMap(a => Either.of(a + 5)); // Failure('error')
8
9
Either.of(5).map(a => b => a + b).ap(Either.of(5)); // Success(10)
10
Either.Failure('error').map(a => b => a + b).ap(Either.of(5)); // Failure('error')

Your own function should return Either to express that there is some error usually in a connection with working with side effects. Consider this example of working with nodejs reading a file:

1
import {Either, either} from '@7urtle/lambda';
2
import fs from 'fs';
3
4
const readFileEither = file => Either.try(fs.readFileSync(file));
5
6
const data = readFileEither('i/may/not/exist.txt');
7
8
data.isFailure() ? 'there is an error ' + data.value : 'your data ' + data.value;

Or consider an example of working with DOM where querySelector throws an error if its selector has the wrong format:

1
import {Either, isNothing, flatMap, compose} from '@7urtle/lambda';
2
3
document.querySelector('@'); // Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '@' is not a valid selector.
4
5
const querySelectorEither = selector => Either.try(() => document.querySelector(selector));
6
const getClientHeightEither = flatMap(value => isNothing(value) ? Either.Failure('Element does not exist.') : Either.Success(value.clientHeight));
7
const selectHeightEither = compose(getClientHeightEither, querySelectorEither);
8
9
selectHeightEither('@'); // Failure('Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '@' is not a valid selector.')
10
selectHeightEither('#idontexist'); // Failure('Element does not exist.')
11
selectHeightEither('#iexist'); // Success(333);
12
13
const testEither =
14
either
15
(error => 'There is an error: ' + error)
16
(value => 'Your result is: ' + value);
17
18
testEither(selectHeightEither('#iexist')); // => 'Your result is: 333'

It depends on the Case

Case is a monad representing a simple map with a matching mechanism. You can use use it as a replacement for a switch with monad enhancements.

1
import {Case} from '@7urtle/lambda';
2
3
// Case expect key-value pairs as its input
4
const myCase = Case.of([
5
[1, 'one'],
6
['key', 'value'],
7
['_', 'fallback'] // '_' represents default fallback option
8
]);
9
10
// you reach a value by matching keys using Case.match
11
myCase.match(1); // => 'one'
12
myCase.match('key'); // => 'value'
13
myCase.match('nope'); // => 'fallback'
14
15
// if no fallback is defined and no key is matched, we return undefined
16
Case.of([]).match('nope'); // => undefined

Case allows you to safely map over its value as the mapped function is applied only after match is called.

1
import {Case, upperCaseOf, liftA2} from '@7urtle/lambda';
2
3
Case.of([['key', 'value']]).map(upperCaseOf).match('key'); // => 'VALUE'
4
5
// you can merge Cases together by using flatMap
6
Case.of([[1, 'I am']]).flatMap(a => Case.of([[1, a + ' a turtle']]).match(1); // => 'I am a turtle'
7
8
// case is also an applicative so you can use partial application
9
const add = a => b => a + b;
10
liftA2(add)(Case.of([[1, 1]]))(Case.of([[1, 2]])).match(1); // => 3

Trigger dangerous SyncEffect lazily

SyncEffect is a wrapper for dangerous functions adding lazy evaluation through its trigger. That means that the wrapped around function is not executed until a trigger is called. SyncEffect serves as a monad to signal that there is some potential dangerous side effect that is not handled. SyncEffect can be called the IO monad in other languages or frameworks. The use of the monad is quite simple:

1
import {SyncEffect} from '@7urtle/lambda';
2
3
// we create SyncEffect that expects a number from 0 to 1
4
// and based on that, it returns a value or throws an error
5
const throwError = () => {throw 'random failure'};
6
const dangerousFunction = value => value > 0.5 ? 'random success' : throwError();
7
const mySyncEffect = SyncEffect.of(dangerousFunction);
8
9
// when you are ready, you can call trigger to trigger the side effect
10
// nothing is executed until the trigger is called
11
// if you pass value to a trigger it is used a an argument for the wrapped around function
12
mySyncEffect.trigger(Math.random());
13
// => returns 'random success' or throws 'random failure' depending on Math.random() value

SyncEffect allows you to safely map over its value as the mapped function is applied only after trigger is called.

1
import {SyncEffect, upperCaseOf, liftA2} from '@7urtle/lambda';
2
3
const throwError = () => {throw 'random failure'};
4
const dangerousFunction = value => value > 0.5 ? 'random success' : throwError();
5
const mySyncEffect = SyncEffect.of(dangerousFunction);
6
7
// as a functor the value inside is safely mappable
8
// map doesn't execute in case of an error and nothing executes until a trigger is called
9
mySyncEffect
10
.map(value => upperCaseOf(value))
11
.trigger(Math.random());
12
// => returns 'RANDOM SUCCESS' or throws 'random failure' depending on Math.random() value
13
14
// as a monad SyncEffect can be safely flat mapped with other SyncEffects
15
// flatMap doesn't execute in case of an error and nothing executes until a trigger is called
16
SyncEffect.of(() => '7turtle').flatMap(a => SyncEffect.of(() => a + 's')).trigger();
17
18
// as an applicative functor you can apply SyncEffects to each other especially using liftA2 or liftA3
19
const add = a => b => a + b;
20
liftA2(add)(SyncEffect.of(() => 1)(SyncEffect.of(() => 2)).trigger(); // => 3
21
SyncEffect.of(() => add).ap(SyncEffect.of(() => 1)).ap(SyncEffect.of(() => 2)).trigger(); // => 3

You can use SyncEffect in a similar situations as the monad Either. The difference is that you are deferring handling the potential negative effects.

1
import {SyncEffect, liftA2, Either} from '@7urtle/lambda';
2
3
const getElementHeightSyncEffect = selector => SyncEffect.of(document.querySelector(selector).clientHeight);
4
getElementHeightSyncEffect('#iexist'); // => 333
5
getElementHeightSyncEffect('#idontexist'); // Uncaught TypeError: Cannot read property 'clientHeight' of null
6
7
const addAp = LiftA2(a => b => a + b);
8
9
addAp(getElementHeightSyncEffect('@'))(getElementHeightSyncEffect('#iexist')).trigger();
10
// 'Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '@' is not a valid selector.'
11
12
you can still use the monad Either to handle the potential error state
13
const getElementHeightEither = selector => Either.try(getElementHeightSyncEffect(selector).trigger);
14
getElementHeightEither('@').isFailure(); // => true

Master lazy asynchronous AsyncEffect

AsyncEffect is a wrapper for dangerous asynchronous functions adding lazy evaluation through its trigger. That means that the wrapped around function is not executed until a trigger is called. AsyncEffect is similar to Promise because its trigger also expects reject and resolve functions that are called based on AsyncEffect execution. Reject function is also called if there is an error thrown. AsyncEffect can also be called Future monad in other libraries or languages.

1
import {AsyncEffect, log} from '@7urtle/lambda';
2
3
// we create AsyncEffect that expects a number from 0 to 1
4
// and based on that, it resolve or rejects 10 milliseconds after it is triggered
5
const myAsyncEffect = AsyncEffect
6
.of(reject => resolve =>
7
setTimeout(() => Math.random() > 0.5 ? resolve('random success') : reject('random failure'), 10)
8
);
9
10
// when you are ready, you can call trigger to trigger the side effect
11
// nothing is executed until the trigger is called
12
myAsyncEffect
13
.trigger
14
(error => log(error))
15
(result => log(result));
16
// => logs 'random success' or 'random failure' depending on Math.random() value
17
18
// you can also turn AsyncEffect into a JavaScript Promise
19
myAsyncEffect
20
.promise()
21
.then(result => log(result), error => log(error));
22
// => logs 'random success' or 'random failure' depending on Math.random() value
23
24
// thrown exceptions lead AsyncEffect to reject
25
AsyncEffect
26
.of(() => {
27
throw 'error';
28
})
29
.trigger(log)(log);
30
// => logs 'error'

AsyncEffect allows you to safely map over its value as the mapped function is applied only after trigger is called.

1
import {AsyncEffect, upperCaseOf, liftA2} from '@7urtle/lambda';
2
3
const myAsyncEffect = AsyncEffect
4
.of(reject => resolve =>
5
setTimeout(() => Math.random() > 0.5 ? resolve('random success') : reject('random failure'), 10)
6
);
7
8
// alternatively you could make it from an existing JavaScript Promise
9
const myPromise = new Promise((resolve, reject) =>
10
setTimeout(() => Math.random() > 0.5 ? resolve('random success') : reject('random failure'), 10)
11
);
12
const promiseAsyncEffect = AsyncEffect.ofPromise(myPromise);
13
14
// as a functor the value inside is safely mappable
15
// map doesn't execute in case of an error and nothing executes until a trigger is called
16
myAsyncEffect
17
.map(value => upperCaseOf(value))
18
.trigger(log)(log);
19
// => logs 'RANDOM SUCCESS' or 'random failure' depending on Math.random() value
20
21
// as a monad AsyncEffect can be safely flat mapped with other AsyncEffects
22
// flatMap doesn't execute in case of an error and nothing executes until a trigger is called
23
AsyncEffect
24
.of(reject => resolve => resolve('7urtles'))
25
.flatMap(a => AsyncEffect.of(reject => resolve => resolve(a + 's')))
26
.trigger(log)(log);
27
// => logs '7urtles'
28
29
// as an applicative functor you can apply AsyncEffects to each other especially using liftA2 or liftA3
30
const add = a => b => a + b;
31
const AS1 = AsyncEffect.of(reject => resolve => resolve(1));
32
const AS2 = AsyncEffect.of(reject => resolve => resolve(2));
33
liftA2(add)(AS1)(AS2); // => resolve(3)

You can use AsyncEffect as an elegant wrapper around your ajax calls or work asynchronously with file reading/writing:

1
import {AsyncEffect} from '@7urtle/lambda';
2
import axios from 'axios';
3
import fs from 'fs';
4
5
// axios example
6
const getFromURL = url => AsyncEffect.ofPromise(axios.get(url));
7
8
getFromURL('/my/ajax/url')
9
.trigger
10
(error => log(error))
11
(result => log(result.data));
12
13
// reading file example
14
const readFile => input =>
15
AsyncEffect
16
.of(reject => resolve =>
17
fs.readFile(input, (err, data) =>
18
err ? reject(err) : resolve(data)
19
)
20
);
21
22
readFile('./file.txt')
23
.trigger
24
(error => log(error))
25
(result => log(result));

Ask For Help

If you have any questions or need any help, please leave a message on @7urtle/lambda GitHub Issues to get quick assistance. If you find any errors in @7urtle/lambda or on the website please reach out.

Get Started

Install @7urtle/lambda with NPM or add it directly to your website.