Functional Programming Patterns: A Cookbook
This article targets an audience that’s graduating from functional libraries like ramda
to using Algebraic Data Types. We’re using the excellent crocks
library for our ADTs and helpers, although these concepts may apply to other ones as well. We’ll be focusing on demonstrating practical applications and patterns without delving into a lot of theory.
Safely Executing Dangerous Functions
Let’s say we have a situation where we want to use a function called darken
from a third-party library. darken
takes a multiplier, a color and returns a darker shade of that color.
Pretty handy for our CSS needs. But it turns out that the function is not as innocent as it seems. darken
throws errors when it receives unexpected arguments!
This is, of course, very helpful for debugging — but we wouldn’t want our application to blow up just because we couldn’t derive a color. Here’s where tryCatch
comes to the rescue.
tryCatch
executes the provided function within a try-catch block and returns a Sum Type called Result
. In its essence, a Sum Type is basically an “or” type. This means that the Result
could be either an Ok
if an operation is successful or an Error
in case of failures. Other examples of Sum Types include Maybe
, Either
, Async
and so on. The either
point-free helper breaks the value out of the Result
box, and returns the CSS default inherit
if things went south or the darkened color if everything went well.
Enforcing Types using Maybe Helpers
With JavaScript, we often run into cases where our functions explode because we’re expecting a particular data type, but we receive a different one instead. crocks
provides the safe
, safeAfter
and safeLift
functions that allow us to execute code more predictably by using the Maybe
type. Let’s look at a way to convert camelCased strings into Title Case.
We’ve created a helper function match
that uses safeAfter
to iron out String.prototype.match
’s behavior of returning an undefined
in case there are no matches. The isArray
predicate ensures that we receive a Nothing
if there are no matches found, and a Just [String]
in case of matches. safeAfter
is great for executing existing or third-party functions in a reliable safe manner.
(Tip: safeAfter
works really well with ramda
functions that return a | undefined
.)
Our uncamelize 🐪
function is executed with safeLift(isString)
which means that it’ll only execute when the input returns true for the isString
predicate.
In addition to this, crocks also provides the prop
and propPath
helpers which allow you to pick properties from Object
s and Array
s.
This is great, especially if we’re dealing with data from side-effects that are not under our control like API responses. But what happens if the API developers suddenly decide to handle formatting at their end?
Runtime errors! We tried to invoke the toFixed
method on a String, which doesn’t really exist. We need to make sure that bankBalance
is really a Number
before we invoke toFixed
on it. Let’s try to solve it with our safe
helper.
We pipe the results of the prop
function to our safe(isNumber)
function which also returns a Maybe
, depending on whether the result of prop
satisfies the predicate. The pipeline above guarantees that the last map
which contains the toFixed
will only be called when bankBalance
is a Number
.
If you’re going to be dealing with a lot of similar cases, it would make sense to extract this pattern as a helper:
Using Applicatives to keep Functions Clean
Often times, we find ourselves in situations where we would want to use an existing function with values wrapped in a container. Let’s try to design a safe add
function that allows only numbers, using the concepts from the previous section. Here’s our first attempt.
This does exactly what we need, but our add
function is no longer a simple a + b
. It has to first lift our values into Maybe
s, then reach into them to access the values, and then return the result. We need to find a way to preserve the core functionality of our add
function while allowing it to work with values contained in ADTs! Here’s where Applicative Functors come in handy.
An Applicative Functor is just a like a regular functor, but along with map
, it also implements two additional methods:
of :: Applicative f => a -> f a
The of
is a completely dumb constructor, and lifts any value that you give it into our data type. It’s also referred to as pure
in other languages.
And here’s where all the money is — the ap
method:
ap :: Apply f => f a ~> f (a -> b) -> f b
The signature looks very similar to map
, with the only difference being that our a -> b
function is also wrapped in an f
. Let’s see this in action.
We first lift our curried add
function into a Maybe
, and then apply Maybe a
and Maybe b
to it. We’ve been using map
so far to access the value inside a container and ap
is no different. Internally, it map
s on safeNumber(a)
to access the a
and applies it to add
. This results in a Maybe
that contains a partially applied add
. We repeat the same process with safeNumber(b)
to execute our add
function, resulting in a Just
of the result if both a
and b
are valid or a Nothing
otherwise.
Crocks also provides us the liftA2
and liftN
helpers to express the same concept in a pointfree manner. A trivial example follows:
We shall use this helper extensively in the section Expressing Parallelism
.
Tip: Since we’ve observed that ap
uses map
to access values, we can do cool things like generating a Cartesian product when given two lists.
Using Async for Predictable Error Handling
crocks
provides the Async
data type that allows us to build lazy asynchronous computations. To know more about it, you can refer to the extensive official documentation here. This section aims to provide examples of how we can use Async
to improve the quality of our error reporting and make our code resilient.
Often, we run into cases where we want to make API calls that depend on each other. Here, the getUser
endpoint returns a user entity from GitHub and the response contains a lot of embedded URLs for repositories, stars, favorites and so on. We will see how we can design this use case with using Async
.
The usage of the maybeToAsync
transformation allows us to use all of the safety features that we get from using Maybe
and bring them to our Async
flows. We can now flag input and other errors as a part of our Async
flows.
Using Monoids Effectively
We’ve already been using Monoids when we perform operations like String
/Array
concatenation and number addition in native JavaScript. It’s simply a data type that offers us the following methods.
concat :: Monoid m => m a -> m a -> m a
concat
allows us to combine two Monoids of the same type together with a pre-specified operation.
empty :: Monoid m => () => m a
The empty
method provides us with an identity element, that when concat
ed with other Monoids of the same type, would return the same element. Here’s what I’m talking about.
By itself, this doesn’t look very useful, but crocks
provides some additional Monoids along with helpers mconcat
, mreduce
, mconcatMap
and mreduceMap
.
The mconcat
and mreduce
methods take a Monoid and a list of elements to work with, and apply concat
to all of their elements. The only difference between them is that mconcat
returns an instance of the Monoid while mreduce
returns the raw value. The mconcatMap
and mreduceMap
helpers work in the same way, except that they accept an additional function that is used to map over every element before calling concat
.
Let’s look at another example of a Monoid from crocks
, the First
Monoid. When concatenating, First
will always return the first, non-empty value.
Using the power of First
, let’s try creating a function that attempts to get the first available property on an object.
Pretty neat! Here’s another example that tries to create a best-effort formatter when provided different types of values.
Expressing Parallelism in a Pointfree manner
We might run into cases where want to perform multiple operations on a single piece of data and combine the results in some way. crocks
provides us with two methods to achieve this. The first pattern leverages Product Types Pair
and Tuple
. Let’s look at a small example where we have an object that looks like this:
{ ids: [11233, 12351, 16312], rejections: [11233] }
We would like to write a function that accepts this object and returns an Array
of ids
excluding the rejected ones. Our first attempt in native JavaScript would look like this:
This of course works, but it would explode in case one of the properties is malformed or is not defined. Let’s make getIds
return a Maybe
instead. We use fanout
helper that accepts two functions, runs it on the same input and returns a Pair
of the results.
One of the main benefits of using the pointfree approach is that it encourages us to break our logic into smaller pieces. We now have the reusable helper difference
(with liftA2
, as seen previously) that we can use to merge
both halves the Pair
together.
The second method would be to use the converge
combinator to achieve similar results. converge
takes three functions and an input value. It then applies the input to the second and third function and pipes the results of both into the first. Let’s use it to create a function that normalizes an Array
of objects based on their id
s. We will use the Assign
Monoid that allows us to combine objects together.
Using Traverse and Sequence to Ensure Data Sanity
We’ve seen how to use Maybe
and friends to ensure that we’re always working with the types we expect. But what happens when we’re working with a type that contains other values, like an Array
or a List
for example? Let’s look at a simple function that gives us the total length of all strings contained within an Array
.
Great. We’ve made sure our function always returns a Nothing
if it doesn’t receive an Array
. Is this enough, though?
Not really. Our function doesn’t guarantee that the contents of the list won’t hold any surprises. One of the ways we could solve this would be to define a safeLength
function that only works with strings:
If we use safeLength
instead of length
as our mapping function, we would receive a [Maybe Number]
instead of a [Number]
and we cannot use our sum
function anymore. Here’s where sequence
comes in handy.
sequence
helps swap the inner type with the outer type while performing a certain effect
, given that the inner type is an Applicative. The sequence
on Identity
is pretty dumb — it just map
s over the inner type and returns the contents wrapped in an Identity
container. For List
and Array
, sequence
uses reduce
on the list to combine its contents using ap
and concat
. Let’s see this in action in our refactored totalLength
implementation.
Great! We’ve built a completely bulletproof totalLength
. This pattern of mapping over something from a -> m b
and then using sequence
is so common that we have another helper called traverse
which performs both operations together. Let’s see how we can use traverse
instead of sequence in the above example.
There! It works exactly the same way. If we think about it, our sequence
operator is basically traverse
, with an identity
as the mapping function.
Note: Since we cannot infer inner type using JavaScript, we have to explicitly provide the type constructor as the first argument to traverse
and sequence
.
It’s easy to see how sequence
and traverse
are invaluable for validating data. Let’s try to create a generic validator that takes a schema and validates an input object. We’ll use the Result
type, which accepts a Semigroup on the left side that allows us to collect errors. A Semigroup is similar to a Monoid and it defines a concat
method — but unlike the Monoid, it doesn’t require the presence of the empty
method. We’re also introducing the transformation function maybeToResult
below, that’ll help us interoperate between Maybe
and Result
.
Since we’ve flipped the makeValidator
function to make more suitable for currying, our compose
chain receives the schema that we need to validate against first. We first break the schema into key-value Pair
s, and pass the value of each property to it’s corresponding validation function. In case the function fails, we use bimap
to map on the error, add some more information to it, and return it as a singleton Array
. traverse
will then concat
all the errors if they exist, or return the original object if it’s valid. We could have also returned a String
instead of an Array
, but an Array
feels much nicer.
Thanks to Ian Hofmann-Hicks, Sinisa Louc and Dale Francis for their inputs on this post.