Dipping a toe into functional JS with lodash/fp
Recently I've been making an effort to write JavaScript in a more functional manner. It requires a change of mind in how to approach problems, but I'd like to share some of the basics that I've learnt so far.
I'll also take a look at how lodash/fp makes functional code a pleasant experience.
Why write functionally?
Before going any further let's have a look at some examples of why we might even want to entertain this whole functional idea. The first and sometimes most revealing benefit is switching from explicit to implicit code.
Explicit
Here is how you could square all the members of an array explicitly:
function squareAll(numbers) {
var squared = [];
for (var i = 0; i < numbers.length; i++) {
squared.push(numbers[i] * numbers[i]);
}
return squared;
}
squareAll([1, 2, 3, 4]); // [1, 4, 9, 16]
At every step of the way we have to tell the JS engine exactly what to do and whilst it's nice to be thorough it leaves more room for bugs and unoptimised code.
It's also just plain ugly to read.
Implicit
A more implicit approach is to use Array.map
:
function squareAll(numbers) {
return numbers.map(num => num * num);
}
squareAll([1, 2, 3, 4]); // [1, 4, 9, 16]
Here we're leaving the details of looping and creating a new array to the map
array method, which is easier to read and less error prone. This is much better,
but we still need to call it on the numbers
array manually. Could we do
better?
const map = require('lodash/fp/map');
const squareAll = map(num => num * num);
squareAll([1, 2, 3, 4]); // [1, 4, 9, 16]
Okay, now we're talking!
Using lodash/fp
means the functions are curried for us by default, and the
argument order is swapped around so it goes from the more familiar version of
map(array, function)
to map(function, array)
. We also no longer deal with
the numbers
argument which is a concept known as point-free
programming.
Another nice side effect is little code to read, test and maintain.
But, a lot changed from the explicit for
loop we had earlier, including the
introduction of terms like 'curried' and 'point-free' so now would be
an ideal time to clarify some terminology.
A delicious curry
Currying is where a function expects a certain amount of arguments and if provided with less, it returns another function that is awaiting the remaining arguments.
Only once it receives all expected arguments does a result get returned.
If provided with the expected amount of arguments all at once it works as normal and returns a result.
const curry = require('lodash/fp/curry');
const greet = curry((greeting, name) => `${greeting}, ${name}!`);
// Passing both arguments allows the function to work as normal
greet('Hello', 'Simon'); // Hello, Simon!
// Passing fewer however, returns another function
const sayBye = greet('See ya');
// And once it receives all its arguments, it returns a value
sayBye('John'); // See ya, John!;
Here is how the lodash/fp documentation describes itself:
The lodash/fp module promotes a more functional programming (FP) friendly style by exporting an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods.
With that in mind, we can now see why the last squareAll
example allowed us
to pass the logic to map
first, and then call it with an array later. We could
also have done it one step like so:
map(num => num * num, [1, 2, 3, 4]); // [1, 4, 9, 16]
We can start to appreciate the use of currying when we look at the next section, composition.
Compose yourself
The idea of composing functions (or flow
in lodash) is to build a larger
function out of many smaller ones. The data goes in one end and flows (ah ha!)
through each function until it comes out the other side. Each function in the
chain passes its return value to the next.
Let's see an example using lodash/fp:
const flow = require('lodash/fp/flow');
const escape = require('lodash/fp/escape');
const trim = require('lodash/fp/trim');
const sanitise = flow(escape, trim);
sanitise(' <html> '); // <html>
You can see that we've created a sanitise
function from escape
and trim
and when the HTML string is passed in it flows through these two functions
before being returned as expected. Again, we're just being implicit and
declaring what we want to happen, not how.
How does currying help?
lodash/fp functions are auto-curried, so when given fewer arguments than
expected they give back a function. This works really well with flow
:
const flow = require('lodash/fp/flow');
const get = require('lodash/fp/get');
const isEqual = require('lodash/fp/isEqual');
const data = {
items: 45
};
const hasAllItems = flow(get('items'), isEqual(45));
hasAllItems(data) // true
Here we're configuring the get
and isEqual
functions with the arguments
needed to get the result, and now they're waiting for their final argument which
is the data. Thanks to flow
we can pass that in at one end and let it pass
through each curried function until our expected value comes out at the end.
For composition to work each function should be unary (accept one argument) and return a single value for the next function to consume. It's fine for the first function to be polyadic (takes multiple arguments) as long it returns a single value.
One more example
What about iterating over a set of items and filtering them?
const flow = require('lodash/fp/flow');
const get = require('lodash/fp/get');
const filter = require('lodash/fp/filter');
const isEqual = require('lodash/fp/isEqual');
const items = [
{name: 'baz'},
{name: 'foo'},
{name: 'bar'},
{name: 'bar'},
{name: 'foo'},
];
const getBars = filter(
flow(get('name'), isEqual('bar'))
);
getBars(items); // [{name: 'bar'}, {name: 'bar'}]
The function created by flow
is what is used by filter
to apply to each member
of the array. Each item is passed through the get
and isEqual
and if it
satisfies the condition it ends up in the new array.
Hopefully you're getting the hang of this!
A note on point-free
You may have noticed that in the last two examples you can't see any mention of
data
or items
apart from when it is passed in as an argument. From the
Wikipedia page:
Tacit programming, also called point-free style, is a programming paradigm in which function definitions do not identify the arguments (or "points") on which they operate. Instead the definitions merely compose other functions, among which are combinators that manipulate the arguments.
Without trying we've written some point-free code!
It's not something to worry about too much, but keeping it in mind has helped me transform code like this:
function isSuccess(response) {
if (response.status === 200) {return true;}
}
isSuccess(response);
Into something more concise:
const isSuccess = flow(get('status'), isEqual(200));
isSuccess(response);
It's a useful guideline to follow.
Pure functions
The last topic to look at is the concept of function purity. This is a function that doesn't depend on state outside of its scope, and doesn't modify it either. Its only jurisdiction is that of the arguments passed in, and even then it should never mutate this data, but return new versions.
Not only does this make testing simpler (you can just pass different arguments and test the results without needing to mock global objects) but it makes code more predictable.
Here's an example where a function mutates its input:
const data = {
foo: 'bar'
};
function sendData(data) {
data.baz = 'test';
API.send(data);
}
function renderData(data) {
// render it, but now I have a baz property?
}
sendData(data);
renderData(data);
sendData
needs to add an additional property before sending the payload to an
endpoint. This causes problems for renderData
as objects are passed by
reference so
now when it comes to render the payload it has an additional baz
property that
should not be there.
The correct approach would be to create a copy and leave the input untouched:
function sendData(data) {
const newData = assign({}, data, {baz: 'test'});
API.send(newData);
}
Functions in lodash like
pick
,omit
,map
etc will always return new copies of the data.
It won't be possible to always write pure functions, especially if you're
interacting with things like document
or window
but it's another thing to
keep in mind.
Practical examples
Well done for shunning the TL:DR crowd and making it this far!
After wading through all the same topics I found it hard to apply them to my code as I wasn't often squaring numbers, or greeting users.
Thankfully it's quite easy to start writing JS in a functional way, even if it's just in small doses. So now I'll go through some small examples that will hopefully inspire you to carry on.
Extracting props in a React component
{
"user": {
"profile": {
"username": "simonsmith",
"age": "31",
"img": "http://image.com",
"gender": "male"
}
}
}
// Returns {username: 'simonsmith', img: 'http://image.com', age: '31'}
const getUserProps = flow(
getOr({}, 'user.profile'),
pick(['username', 'img', 'age'])
);
const User = props => {
const {username, age, img} = getUserProps(props);
return (
<div className="User">{username}</div>
);
};
module.exports = User;
This is a pattern I find myself using often in React, grabbing the data you need
from some props. The use of getOr
here allows us to set a default value if
user.profile
is undefined.
Without JSX
If you're not using JSX you can take this a step further and flow the props
directly into a factory function. Here's an example with one from React.DOM
:
const flow = require('lodash/fp/flow');
const pick = require('lodash/fp/pick');
const React = require('react');
const {img} = React.DOM;
const imgWithProps = flow(
pick(['src', 'alt', 'className']),
img
);
const Component = props => imgWithProps(props);
I've become quite fond of this pattern.
Turning an object into query string parameters
const map = require('lodash/fp/map').convert({'cap': false});
const join = require('lodash/fp/join');
const flow = require('lodash/fp/flow');
const toQueryString = flow(
map((value, key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
}),
join('&')
);
toQueryString({foo: 'bar', baz: 'moo'}) // foo=bar&baz=moo
One thing that may cause some confusion when coming to lodash/fp
from regular
lodash
is how the arguments are capped for certain functions. That means that
whilst you may be used to accessing the value
and key
in map
it will only
provide you with value
by default in lodash/fp
.
In the above code I'm using .convert
to change the behaviour and allowing
toQueryString
to work as expected.
Read more about convert
and capped arguments in the
documentation.
Handling data from an API
The last example is a simplified version of some code from a real project. An
API returned a payload with a meta
object about the response and the body
contained a stringified version of the main payload:
const response = {
data: {
meta: {responseCode: '200', statusMessage: 'OK'},
body: '{"html":{"head":"<head></head>","foot":"<footer></footer>"}}'
}
};
The goal is to extract the HTML from the body
if the response is successful,
otherwise log the error status code and message.
const flow = require('lodash/fp/flow');
const get = require('lodash/fp/get');
const negate = require('lodash/fp/negate');
const toNumber = require('lodash/fp/toNumber');
const isEqual = require('lodash/fp/isEqual');
const logger = require('./logger');
// Reusable functions to reduce repetition of `foo.bar` or `foo.baz`
const getRoot = get('data');
const getMeta = flow(getRoot, get('meta'));
const getResponseCode = flow(getMeta, get('responseCode'), toNumber);
const getStatusMessage = flow(getMeta, get('statusMessage'));
// Using negate to check against any other value than `200`
const isApiFailure = flow(getResponseCode, negate(isEqual(200)));
// `JSON.parse` is also a unary function
const getHtml = flow(getRoot, get('body'), JSON.parse, get('html'));
function handleResponse(response) {
if (isApiFailure(response)) {
return logger(getResponseCode(response), getStatusMessage(response));
}
return {
html: getHtml(response)
};
}
The focus here is to prefer many small functions that do one thing. Code written this way is simple to unit test, and in this example we pass a lot of the work to lodash and gain extra confidence knowing that it is already well tested and performant.
A note on Ramda
The other well known alternative to lodash/fp is Ramda. It works in a similar way with auto-curried, data last functions. Why not advocate that as well? I've tried using it, and it's great but here are the reasons I stuck with lodash:
- It's very easy to switch to lodash/fp gradually. The benefit of being able to
switch from
lodash/assign
tolodash/fp/assign
is enormous. It means you can start write functionally in areas of your code base without refactoring every module. - lodash/fp comes with mappings to Ramda functions, so there is no need to relearn the API immediately if you're a Ramda user.
- Performance is a big concern for lodash, which gives confidence that you can use its functions quite liberally to achieve point-free code.
Wrapping up
Hopefully this has shed some light on how you can start implementing functional programming into your own projects. This post by no means covers everything, so I've compiled a list of excellent resources from very smart people to help you go further on your functional journey.