The goal of this cheat sheet is to make it easy for newcomers and experienced developers to work with the Sanctuary library by describing common patterns and best practices.
WARNING: the information in this cheat sheet is by no means a comprehensive collection of all the library functions and types. Nor are the examples the only or even the best way of how to use them in your code. Keep this in mind and also dive into other resources. I highly recommend reading Things I wish someone had explained about Functional Programming and the Fantas, Eel, and Specification.
- Read-Eval-Print-Loop - try out Sanctuary
- Function definition
- Type definition - create your own functional types
- Piping - connecting function outputs to function inputs and avoid intermediate variables
- Print debugging - inspecting intermediate values
- Branching - handling if-else cases
- Promises - back to the Future
- map or chain?
- filter - remove unneeded values
- reduce - accumulate values
- Error handling
- Pair - storing key-value pairs
- Libraries - little helpers
- Resources - additional things that might be helpful
A web-based Sanctuary-only REPL is available online, start typing in the green box.
To quickly get a local Sanctuary and Fluture REPL, open
the developer tools in your browser (keyboard shortcut Ctrl-Shift-i
) and
execute this instruction:
let S; let def; let F;
import("https://deno.land/x/[email protected]/repl.js").then(l => {S = l.S; def = l.def; F = l.F;});
To quickly get a local Sanctuary and Fluture REPL, run this command:
deno repl --eval 'import {S, def, F} from "https://deno.land/x/[email protected]/repl.js"'
There are three aspects to defining functions:
- Define the parameters - one after the other
- Define the processing steps
- Define the function signature with types
In functional programming functions are usually curried. This means that a function only takes one parameter. If a function requires more than one parameter it should be defined as a function that takes one parameter and returns a functional that requires another parameter.
Fortunately, JavaScript's arrow functions make it really easy to create curried functions:
const myfunction = (parameter1) => (parameter2) => (parameter3) => {
// the function body
};
In sanctuary there's a convenient way of defining the processing steps - the
pipe
function. pipe
takes a list of functions and it passes
the output value of one function as the input value into the following function.
See
Piping
for more information:
const myfunction = (parameter1) =>
S.pipe([
// first processing step
doA,
// second processing step
doB,
// ...
doC,
])(parameter1);
For very simple functions defining processing steps might be enough. However, to get all the benefits from sanctuary's type checking functionality the function signature needs to be defined the sanctuary way. Take a look at the built-in types:
// define a def function that makes it easy to create functions with
type checks
const $ = require("sanctuary-def");
const def = $.create({
checkTypes: process.env.NODE_ENV === "development",
env,
});
// add :: Number -> Number -> Number
const add =
def ('add') // name
({}) // type-class constraints
([$.Number, $.Number, $.Number]) // input and output types
(x => y => x + y); // implementation
TODO daggy
The types that can be used by functions need to be first defined. Sanctuary has
a number of constructors for defining types. Take a look at sanctuary's
Type constructors.
Here is a very simple one that defines an integer. Keep in mind that a
documentation URL is required where more information can be found about the
type - the project's REAMDE.md
is a good place to keep the type definition
documentation at:
const Integer = $.NullaryType(
// name
"Integer",
)(
// documentation URL
"http://example.com/my-package#Integer",
)(
// supertypes
[],
)(
// predicate values need to satisfy
(x) =>
typeof x === "number" &&
Math.floor(x) === x &&
x >= Number.MIN_SAFE_INTEGER &&
x <= Number.MAX_SAFE_INTEGER,
);
Functions often contain a lot of calls to other functions. The intermediate values of the function calls are stored in variables are passed again to other function calls. It might look something like this:
const myfunction = (parameter1) => (parameter2) => (parameter3) => {
const resA = doA(parameter1);
const resB = doB(parameter2)(resA);
const resC = doC(parameter3)(resB);
return resC;
};
This could be optimized with the pipe
function by removing the
variables and feeding the intermediate results directly into the next function:
const myfunction = (parameter1) => (parameter2) => (parameter3) =>
S.pipe([
doA,
// output of doA is piped as input into doB
doB(parameter2),
doC(parameter3),
])(parameter1);
The goal of print debugging is to peek into a function execution chain and learn about intermediate results.
Example, given the following function - how to inspect the return value of
doA
?
const myfunction = S.pipe([
// some function calls
doA,
doB,
doC,
]);
Solution, define a log
function that prints a message and the received value
and returns the value. Then add the log
function between doA
and doB
:
const log = (msg) => (value) => {
console.log(msg, value);
return value;
};
const myfunction = S.pipe([
doA,
// insert log function
log("Return value of do3:"),
doB,
doC,
]);
In a function there is often the need to handle two cases differently:
const myfunction = (parameter1) => {
const res = computeSomething(parameter1);
if (res > 0) {
doA(res);
} else {
doB(res);
}
// further processing
};
In Sanctuary it could be done with the ifElse
function
as follows:
const myfunction = (parameter1) =>
S.pipe([
computeSomething,
S.ifElse((res) => res > 0)(doA)(doB),
// further processing
])(parameter1);
This could get ugly if there are more cases that need to be distinguished, e.g.
res < 0
, res < 10
and res >= 10
:
const myfunction = (parameter1) =>
S.pipe([
computeSomething,
S.ifElse((res) => res < 0)(doB)(S.ifElse((res) => res < 10)(doA))(doC),
])(parameter1);
In this case it might be easier to TODO ...?
Sanctuary doesn't provide special handling for
Promises
. However, since they're used all over the place in
JavaScript it would be great to deal with them in a functional way. There's a
functional Promises
library for this:
Fluture
Here's the official Fluture sanctuary integration. The important lines are:
import sanctuary from "sanctuary";
import { env as flutureEnv } from "fluture-sanctuary-types";
const S = sanctuary.create({
checkTypes: true,
env: sanctuary.env.concat(flutureEnv),
});
import { attemptP, encaseP, fork, parallel } from "Fluture";
The fork
call needs to be present in the program and there should be
ideally only one fork call. fork
processes the Promise
.
Without fork
no processing of Futures
takes place.
fork(
// error case
log("rejection"),
)(
// resolution case
log("resolution"),
)(attemptP(() => Promise.resolve(42)));
There are two main helper functions by Fluture to deal with
Promises
: attemptP
and encaseP
.
attemptP
takes a function that doesn't take a parameter and turns
it into a Future
, e.g.:
attemptP(() => Promise.resolve(42));
encaseP
takes a function that takes one parameter and turns it into
a Future
, e.g.:
encaseP(fetch)("https://api.github.com/users/Avaq");
The main question is how do we deal with Futures in pipe
. There are
two important cases to keep in mind: map or chain?. Either we
process the Future with map
(2) - in this case no knowledge about the
Future is required by the function that receives the value - or with
chain
(3) - in this case the Future is consumed and a new future needs to
be returned by the function.
If we forget to use map
or chain
in a function call (1), the
function receives the unfinished Future. It's like acting on a
Promise
without calling .then()
or await
on it.
const myfunction = S.pipe([
encaseP(fetch),
log("Try to log the output of fetch:"), // 1
S.map(log("Log the output of fetch:")), // 2
S.map(extractURL),
S.chain(encaseP(fetch)), // 3
]);
fork(log("rejection"))(log("resolution"))(
myfunction("https://api.github.com/users/Avaq"),
);
It's also possible to process multiple Futures
in a functional way.
For example, multiple long-running computations should to be performed.
parallel
provides this functionality and controls the number of
parallel executions with the first parameter:
const myLongRunningFunction = (x) => {
// computation take 1 sec
return new Promise((resolve, reject) => setTimeout(resolve, 1000, x * 2));
};
fork(log("rejection"))(log("resolution"))(
S.pipe([
// 5 Futures are created
S.map(encaseP(myLongRunningFunction)),
// 2 Futures are processed in parallel until all are resolved
parallel(2),
])([1, 2, 3, 4, 5]),
);
Unlike Promises
, Futures
don't execute the contained
function unless fork
is called on it. This makes it possible to stop a
Future
or to never execute it if not needed. The functionality is
described in detail in the Cancellation documentation.
There are these two different functions, map
and chain
, that
look very similar. However, using one over the other is sometimes advantageous.
map
is defined by the Functor
class type. Every
Functor
implements map
. Functors
are often
arrays and map
maps a function over every element of the array.
Example, add 1
to every element in an array of numbers:
const numbers = [1, 2, 3];
const add = (number1) => (number2) => number1 + number2;
S.map(add(1))(numbers);
// result: [2, 3, 4]
In addition, something like a Pair
or a Promise
could
also be a Functor
. In this case map
maps over the value,
e.g. the result of a Promise
or the value of a Pair
.
const pair = S.Pair("a")(1);
const add = (number1) => (number2) => number1 + number2;
S.map(add(1))(pair);
// result: Pair ("a") (2)
As you can see in the example, the add
doesn't concern itself with the inner
workings of the data type but just operates on the value. map
does the
heavy lifting of getting the Functors
value out and wrapping the
modified value back in a Functor
. This is very convenient because
it makes functions easily applicable to all kinds of Functors
.
However, sometimes this is intelligence of putting the returned value back in a
Functor
works against us. For example, we want to parse an integer
from string but only want to return a Just
value if the integer is
greater than 10 otherwise Nothing
. If we tried to do this with
map
we'd end up with this result:
S.pipe([
S.parseInt(10),
S.map(S.ifElse((v) => v > 10)(S.Just)((v) => S.Nothing)),
])("100");
// result: Just (Just (100))
There are now two nested Just
data types. As you can see from the
implementation, the function that's called by map
already uses the
complex data type Pair
(implemented by Just
and
Nothing
). Therefore, if since we pass a Pair
into the
function and the function returns a Pair
, we don't needmap
's
feature of wrapping the returned value in the passed in Functor
.
chain
as defined by the Chain class type does exactly that, it
expects the function to properly wrap the return value in the
Functor
. This is important when working with Promises
to ensure that we're not wrapping an unresolved Promise
inside a
resolved Promise
but return the unresolved Promise
so
we can wait for its completion:
S.pipe([
S.parseInt(10),
S.chain(S.ifElse((v) => v > 10)(S.Just)((v) => S.Nothing)),
])("100");
// result: Just (100)
If you receive a value that's wrapped twice in the same type we can use
join
to remove one layer of wrapping:
S.pipe([
S.parseInt(10),
S.map(S.ifElse((v) => v > 10)(S.Just)((v) => S.Nothing)),
S.join, // added join
])("100");
// result: Just (100)
Note that the added join
plays nicely in case Nothing
is
returned by parseInt
:
S.pipe([
S.parseInt(10),
S.map(S.ifElse((v) => v > 10)(S.Just)((v) => S.Nothing)),
S.join, // added join
])("invalid100");
// result: Nothing
When composing function calls with pipe
it's common that arrays of
values are processed. map
is great for transforming array elements with
the help of other functions. However, sometimes the list of array elements needs
to be reduced before processing them further. For example, null
values or
Nothing
values need to be removed or numbers that are lower than a
certain threshold. This can be easily done with filter
that takes a
predicate / filter function:
S.filter(
// predicate function that's applied to input values
(x) => x > 3,
)(
// the input values
[1, 2, 3, 4, 5],
);
// [ 4, 5 ]
In the same way as filter
, reduce
operates on an array
of values and transforms + collects them into an accumulated/reduced new value.
This concept of reducing values is so powerful that map
and
filter
can be expressed with reduce
. However, expressing
map
or filter
via reduce
is more difficult to
read than using the predefined functions. Therefore, we'll stick to simple
reduction feature here. For example, the values of an array could be summed up
with reduce
:
S.reduce(
// function that performs the accumulation / reduction of values
(acc) => (x) => acc + x,
)(
// start value for acc
0,
)(
// the input values
[1, 2, 3, 4, 5],
);
// result: 15
When processing data sometimes the data doesn't conform to the requirements and an error is raised. In Sanctuary there are multiple ways of handling errors, a few of them are explored here:
A function might not be able to operate on all possible input values. For
example, the parseInt
function takes a string and tries to parse
an integer from it. When it fails to parse the string the function could return
null
, undefined
or NaN
but this leaves lots of
room for interpretation as it's not clear whether the function was able to
process the input properly.
Instead, a Maybe
type could be returned that wraps the actual result
in either a Just
or a Nothing
object. When wrapping the
return value in a Maybe
object further processing steps graciously
deal with the result. For example, map
only executes the transformation
function when a Just
object is returned:
const myParseInt = (str) => {
const res = parseInt(str);
if (isNaN(res)) {
return S.Nothing;
}
return S.Just(res);
};
S.show(
S.map(
S.pipe([
// call to function that produces a Maybe result object
myParseInt,
// further processing
S.map((x) => x + 10),
]),
)(["1", "invalid1"]),
);
// result: [Just (11), Nothing]
Additional functions exist for handling Maybe
objects.
Another programming challenge is to deal with errors, for example when an
attempted division by zero. Instead of throwing
an Error
,
Sanctuary offers the Either
type that can be a
Right
object that includes the successful result or a Left
object that includes the error.
Either
is different from Maybe
in that Left
contains additional data for processing and potentially recovering from the
error while Nothing
contains no data.
const myDiv = (num) => (divider) => {
if (divider === 0) {
return S.Left("Division by zero.");
}
return S.Right(num / divider);
};
S.show(
S.map(
S.pipe([
// call to function that produces an Either result object
myDiv(25),
// further processing
S.map((x) => x + 10),
]),
)([5, 0]),
);
// result: [Right (15), Left ("Division by zero.")]
Additional functions exist for handling Either
objects.
When there are multiple subtypes to deal with like Left
and
Right
it would be handy to be able to map over both options.
bimap
provides this feature so we can begin handling the failure:
const myDiv = (num) => (divider) => {
if (divider === 0) {
return S.Left("Division by zero.");
}
return S.Right(num / divider);
};
S.show(
S.map(
S.pipe([
// call to function that produces an Either result object
myDiv(25),
// further processing
S.bimap(S.toUpper)((x) => x + 10),
]),
)([5, 0]),
);
// result: [Right (15), Left ("DIVISION BY ZERO.")]
mapLeft
is another option for just interacting with the error case.
For Futures
, coalesce
and mapRej
are the
respective functions for dealing with rejected values.
Sanctuary provides the type Pair
for storing key-value
pairs. Compared to a simple JavaScript Object
({}
), Pair
plays nicely with other functions, e.g. map
and mapLeft
:
const p = S.Pair('balance')(1)
S.show(S.map(x => x * 2)(p))
// result: Pair ("balance") (2)
S.show(S.mapLeft(x => "accountnumber")(p))
// result: Pair ("accountnumber") (1)
- Sanctuary - Refuge from unsafe JavaScript: Sanctuary
- Sanctuary type class overview: Sanctuary Type Classes and Fantasy Land Specification
- Sanctuary type overview: sanctuary-def
- Fluture - Fantasy Land compliant (monadic) alternative to Promises: Fluture
- Most - Monadic stream for reactive programming: Most
- Sanctuary library introduction: Sanctuary, Programming Safely in an Uncertain World
- Sanctuary Abstract Data Type Overview: Sanctuary ADT Matrix
- Introduction to functional programming: Things I wish someone had explained about Functional Programming
- Fantasy Land Spec walk through: Fantas, Eel, and Specification
- Functional programming video tutorial series: Professor Frisby Introduces Composable Functional JavaScript
- Functional Design Patterns: Functional Design Patterns - Scott Wlashin
- Functional programming pattern Monad explained in 100sec: What is a Monad?
- Functional Domain Modeling: Domain Modeling Made Functional - Scott Wlashin
- JavaScript Functional Programming YouTube Channel: Fun Fun Function
- Prof. Frisby's Mostly Adequate Guide to Functional Programming
- Composing Software
- Functional-Light JavaScript
- Category Theory for Programmers
- Awesome List Functional Programming in JavaScript: Awsome FP JS