Resilient JavaScript with Sanctuary

Why I chose Sanctuary?

Because in addition to exposed functions and utilities, it relies on some Algebraic Data types, for instance, Maybe:

http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

Basically, as shown in the picture above, a Maybe might be Just (I see it as a “box” with a wrapped value accessible through e.g. map) and Nothing (yes nothing, there is no null, undefined or similar weird things). Thanks to this, it’s possible to keep a level of safety when handling a “hot potato”.

Along with Maybe, Either is also supported, which is composed by Left (usually handling errors) and Right (usually handling success values).

Now that we have, kind of, laid out the basic concepts, what Sanctuary does actually offer us?

Let’s start from a simple example: parseInt (which accepts 2 parameters — string and radix — and returns a number (if valid) or NaN (if invalid) — oh yes, add it to null and undefined :).

Sanctuary comes in the rescue with the following approach:

parseInt :: Radix -> String -> Maybe Integer

It might be a bit tricky to read when not familiar with this syntax, but it actually takes in an argument per function (radix and string) and returns a Maybe(Integer), which is composed by Just(Integer value) or Nothing (not NaN, null, undefined or whatever).

S.parseInt (10) ('-42')
// Just (-42)S.parseInt (16) ('0xFF')
// Just (255)S.parseInt (16) ('0xGG')
// Nothing

This is nice because we can uniform our data in the flow to be actually something simple to understand.

Another example might be “JSON.parse” where Try/Catch is behind the corner:

parseJson :: (Any -> Boolean) -> String -> Maybe a

This case looks a bit different but you might notice an extra function, which is actually a validator of what we are trying to parse which, when truthy, it returns the valid object wrapped in a Just or, again, Nothing.

S.parseJson (S.is ($.Array ($.Integer))) ('[')
// Nothing

S.parseJson (S.is ($.Array ($.Integer))) ('["1", "2", "3"]')
// Nothing

S.parseJson (S.is ($.Array ($.Integer))) ('[0, 1.5, 3, 4.5]')
// Nothing

S.parseJson (S.is ($.Array ($.Integer))) ('[1, 2, 3]')
// Just ([1, 2, 3])

S.parseJson (_ =>  true) ('["a", "b", "c"]')
// Just (["a", "b", "c"])

N.B. There is no try/catch because that function handles errors for us and it returns Nothing. Amazing!

Let’s move to another hot topic in JS, dot notation.

const foo = {
bar: {
inner: {
thing: {
exists: true
}
}
}
}
;
foo.bar.inner.thing.exists
// true

Even if we are paying super attention to our data, this might break at any time because one property in the chain might not be defined. In order to access deep structures, we can use “S.gets” which helps in handling tricky situations:

gets :: (Any -> Boolean) -> Array String -> a -> Maybe b

const foo = {
bar: {
thing: {
exists: true
}
}
}
;

S.gets (S.is($.Boolean)) (['bar', 'thing', 'exists']) (foo)
// Just(true)

S.gets (S.is($.Number)) (['bar', 'thing', 'exists']) (foo)
// Nothing

S.gets (S.is($.Number)) (['data', 'user', 'name']) ({})
// Nothing

Thanks to this we can achieve two things:

  • Access nested properties in a safe way;
  • Validation of the traversed value;

As usual, if the path is broken or the value is not adhering to what we expect, Nothing is returned.

Another example might be Head and Tail, coming from the Functional Programming field:

S.head ([40, 20, 30, 50])
// Just (40)S.head ([])
// Nothing
S.tail ([40, 20, 30, 50])
// Just ([20, 30, 50])S.tail ([])
// Nothing

It’s not that complex to understand and there are tons of utilities you could use, in a safe way.

What I really rely on all the time is “pipewhich performs a left-to-right composition of a sequence of functions.

S.pipe ([
S.add (1),
Math.sqrt,
S.sub (1)
]) (
99)
// 9

In my humble opinion, this a beautiful way of describing a sequence of steps for a given logic and it reminds me of the Clojure way (with thread-last macros).

For example, we might have this simple algorithm:

S.pipe([
S.map(S.pow(2)), // Map each item to power of 2
S.filter(S.even), // Filter even numbers
S.take(10), // Takes first 10 items
S.map(S.sum) // Sum all values ('map' accesses Maybe)
])
(
S.range(0)(100)) // Creates an array with numbers from 0 to 100
// Just (1140)

First: this is really easy to read and predict. When going through code review this might be like reading a poem and not a tragedy.

Second: yes, this is reacting to edge cases. If we change to “S.range(0)(10)” or an empty Array as input, it will return Nothing because the logic will not be able to take 10 items from the Array (we are filtering even numbers out — thus reducing the available list — ).

What about a more complex example?

A real-world scenario would be traversing and aggregating just a part of a nested data structure. I will lay out one example with the Seach Commits API provided by Github and select only the Commit messages related to a given user. One requirement: logic needs to be rock-solid.

This is some data we (hypothetically) receive — it’s just an example taken from Github API guides and mixed with mocked data):

Our logic might be the following:

  • Take the collection and the user in;
  • Access “items” which contains the results (does it?!);
  • Filter the result items matching the “committer” email with the desired user’s one;
  • Aggregate the Commit messages only and return the found list;
const retrieveCommitMessagesByUser = (commits, user) => {
return S.pipe([

])(

commits);
};

getCommitMessagesByUser(commitsCollection, 'octocat@nowhere.com');

Easy peasy, we can use a function which accepts a list of commits and the desired user’s email, and it does the magic.

So now we can try accessing the items list in a preferred way:

const getCommitsMessagesByUser = (commits, user) => {
return S.pipe([
S.get(S.is($.Array($.Object)))('items'),
])(commits);
};

getCommitsMessagesByUser(commitsCollection, 'octocat@nowhere.com');

// Output: 
// Just ([...truncated items...])

We can use “S.get” in order to access the items in the object and we do a validation specifying that it needs to be an Array of Objects. It will return Just if successful or Nothing when not.

Next, we need to filter the user’s commits:

const committerIs = committer => {
return _ => S.pipe([
S.gets(S.is($.String))(['commit', 'committer', 'email']),
S.equals(S.Just(committer))
])(
_);
};

const getCommitsMessagesByUser = (commits, user) => {
return S.pipe([
S.get(S.is($.Array($.Object)))('items'),
S.map(S.filter(committerIs(user))),
])(commits);
};

getCommitsMessagesByUser(commitsCollection, 'octocat@nowhere.com');

// Output: 
// Just ([...truncated user's items...])

Since the previous instruction is returning a wrapped value, in order to access it again, we’d need to use “map” and “filter” the collection. I created a function in order to perform a deep check. In the end, this is Functional Programming and we are composing/re-using functions across the code.

The returned output is a wrapped value of commit messages related to the user we want.

Now that we have a filtered set of data, we can take the messages only:

const retrieveCommitMessagesByUser = (commits, user) => {
return S.pipe([
S.get(S.is($.Array($.Object)))('items'),
S.map(S.filter(committerIs(user))),
S.map(S.map(S.gets(S.is($.String))(['commit', 'message'])))
])(
commits);
};

retrieveCommitMessagesByUser(commitsCollection, 'octocat@nowhere.com');

// Output:
// Just ([Just ("Create styles.css and updated README"), Just ("Updated eslint configuration"), Nothing])

With the first “map” we are accessing the Maybe and with the second “map” we iterate the items in the list. “S.gets” performs access to “commit.message” and checks if the retrieved value is what we expect: a string. You might notice “Nothing” along with “Just”, because one of the items we have in the collections has the right committer but, for some reason, the message is set to “null” (it’s just mocked data but it might happen with data returned from external APIs so it makes total sense to handle it).

Afterward, let’s clean the output with valid values only:

const retrieveCommitMessagesByUser = (commits, user) => {
return S.pipe([
S.get(S.is($.Array($.Object)))('items'),
S.map(S.filter(committerIs(user))),
S.map(S.map(S.gets(S.is($.String))(['commit', 'message']))),
S.map(S.justs)
])(
commits);
};
retrieveCommitMessagesByUser(commitsCollection, 'octocat@nowhere.com');
// Output:
// Just (["Create styles.css and updated README", "Updated eslint configuration"])

Cool, everything good so far!!

The last step might be unwrapping the value from the Maybe:

const retrieveCommitMessagesByUser = (commits, user) => {
return S.pipe([
S.get(S.is($.Array($.Object)))('items'),
S.map(S.filter(committerIs(user))),
S.map(S.map(S.gets(S.is($.String))(['commit', 'message']))),
S.map(S.justs),
S.fromMaybe([])
])(
commits);
};

retrieveCommitMessagesByUser(commitsCollection, 'octocat@nowhere.com');

// Output:
// [ 'Create styles.css and updated README',
'Updated eslint configuration' ]

S.fromMaybe” is taking the value from Just or, when Nothing, it returns a default value passed in (an empty array in this case).

That’s it!

It might look like a bit odd at the beginning, but when familiar with this approach and with functional programming, this makes sense. Few lines of code are handling our logic and being resilient to edge cases and unhappy paths (something null/undefined, not the type we expect, etc…).

There are tons of use cases and, combined with other technologies and approaches, it brings the whole thing to a different dimension.

I created a REPL playground where you can play with it: