What is it?
There are a lot of formal versions out there that look like the following
Functional Programming or FP is the process of building software by composing pure functions and avoiding shared state, mutable data, and side-effects
There are a bunch of very important concepts in that statement. Let me break this down a bit. To be a pure function the following must be true:
- return values are only determined by input values
- no data mutation takes place i.e. no changes to data state
- no side effects e.g. by calling an external event handler, API or internal context
Not all functions should be pure functions otherwise we would never be able to use observables or make an API call.
So why do we want pure functions? If we want to make repeatable, re-usable, stable building blocks to create more complex repeatable, re-usable stable building blocks we need pure functions otherwise your code can become overly complex and hard to maintain. In more technical terms pure functions are easy to:
- make lazy
- memoize
- make idempotent
- offer referential transparency
- test, debug
- parallelise
- (and most importantly) be read by a human.
Here are some examples just in case I lost you
This is NOT a pure function because it mutates input data and relies on an external variable called ‘taxRate’
const addTaxToProductPrice = (product) => {
product.price = product.price * taxRate
return product;
}
This IS a pure function
const addTaxToProductPrice = (price, taxRate) => {
return price * taxRate;
}
While I completely agree with the above definition of pure functions, I much prefer my simpler version that emphasises its real strength.
Functional Programming allows you to efficiently convert data from one form to another using reusable functional building blocks
What does that really mean? Well if we have, say, some piece of data called Person represented in JSON that has a name, age and date of birth
[
{
name: ‘John’,
age: 37,
dob: 08/03/1980
},
{
name: ‘Bob’,
age: 27,
dob: 08/03/1990
},
{
name: ‘Trevor’,
age: 80,
dob: 08/03/1945
},
]
and say we wanted to filter out all those that can still work, i.e. younger than 65 and I just want their names only.
Using a traditional approach I would write a function like this
const results = [];
for (let i = 0; i < person.length; i++) {
const person = people[i];
if (person.age < 65) {
results.push(person);
}
}
// => [“John”, “Bob”]
or using functional programming I could write
people.filter(person => person.age < 65).map(person => person.name);
// => [“John”, “Bob”]
Hooray! That was pretty simple. I’ve assumed that you’re well versed in ES6 so you’ll understand those arrow functions I just used. If not, then don’t worry, check out this great link which maps ES5 features to ES6.
What’s all the fuss about?
OK, you might be thinking That’s not so flash, I can do that with some for-loop and if-statement blocks with my favourite object orientated language. Well of course you can do that just like we did above and you might be able to optimise it down by a couple of lines, but look at what we just did in a single line. Can you also see its potential? Let’s do some refactoring…
const getPeoplesNames = (people) => people.map(person => person.name);
const getPeopleUnder65 = (pepole) => people.filter(person => person.age < 65);
const result = getPeoplesNames(getPeopleUnder65(people));
// => [“John”, “Bob”]
This is a whole lot more powerful. We have broken down a seemingly complex expression into its functional building blocks which we can re-use in a human readable way. You cannot do this with your for-loop or if-statement blocks in your favourite object orientated language without repeating large amounts of boiler plate code. If we write in a functional way:
- functions become first class citizens that can be assigned, passed around, composed and chained. It is this chaining that makes functional programming easier to read and write and therefore so powerful
- its simplicity makes it harder to write code the wrong way so we end up writing more stable code. Any unit tests we write just need to test our custom code not the underlying implementation of map and filter resulting in, more often than not, fewer lines of unit testing code
So to summarise, we need functional programming so we can write more stable code that is compact and more human readable.
“We want code that describes what we want, not how to get it”
Our code becomes more concise. We are moving basic programming ideas back into the compiler like iterators, temporary variables and caching while focusing on the logic that adds value. There are performance benefits too because most functional programming languages support lazy loading. Say you have an array that is infinitely large and full of numbers. You can ask for the 1,000,000th element without knowing the size of that array.
The Rise of Ramda
OK, now that I’ve convinced you that functional programming is the coolest thing since sliced bread (and if I haven’t, don’t worry, the best is still to come), you’ll be super impressed to learn about how Ramda acts as the Batman utility belt making functional programming even simpler. If I was to sum up Ramda and how I use it in my day to day job, I’d say:
“Ramda makes function chaining and re-use easy”
Let’s look at an example using Ramda to solve our earlier problem to get all people’s names that are under 65 years of age. Instead of ES6’s built in map and filter functions, we’ll use Ramda’s functions compose, filter and map.
import {map, filter, compose} from ‘ramda’;
compose(filter(person => person.age), map(person => person.name))();
// => [“John”, “Bob”]
We could further refactor this to
import {map, filter, compose} from ‘ramda’;
const namesOfPeople = map(person => person.name);
const peopleUnder65 = filter(person => person.age);
const namesOfPeopleUnder65 = compose(peopleUnder65, namesOfPeople);
namesOfPeopleUnder65(people) => // [“John”, “Bob”]
As you can see we use compose to take a chain of functions (filter and map) and return a single function that we can then execute just like … a normal function.
Testing in Ramda
Of course no tool is complete without good testing support. Typically you might test the above code this way (this example uses Jest.)
expect(namesOfPeopleUnder65(people)).toBe([“John”, “Bob”]);
// => Passed
This is great if the test passes however if it fails how do we debug that chain of functions? Ramda’s Tap function to the rescue! R.tap is a curried function that you can insert anywhere in your function chain without any side effects or disruption to data flow. You simply pass it a function like console.log and watch the data pass from one function to the next printed out into your console.
import {map, filter, compose, tap} from ‘ramda’;
const namesOfPeople = map(person => person.name);
const peopleUnder65 = filter(person => person.age);
const namesOfPeopleUnder65 = compose(peopleUnder65, tap(console.log), namesOfPeople);
expect(namesOfPeopleUnder65(people)).toEqual([“John”, “Bob”]);
// => [{“name”:”John”,”age”:37,”dob”:”08/03/1980″},{“name”:”Bob”,”age”:27,”dob”:”08/03/1990″}]
// => Passed
Of course if you’re finding that you’re debugging Ramda quite a log and don’t want to keep adding R.tap everywhere or want better debug output then you’ll be happy to know that there are some handy Ramda wrappers called ramda-debug and tap-debug that make this easy. For example you can simply do the following (in the case of ramda-debug)
import {look} from ‘ramda-debug’;
const R = look.wrap(require(‘ramda’));
…
const getNamesOfPeopleUnder65 = look.fov(R.pipe(getPeopleUnder65, getPeoplesNames));
// => console.log node_modules/ramda-debug/src/look.js:68
// = [ { name: ‘John’, age: 37, dob: ’08/03/1980′ },
// { name: ‘Bob’, age: 27, dob: ’08/03/1990′ },
// { name: ‘Trevor’, age: 80, dob: ’08/03/1945′ } ] → [ ‘John’, ‘Bob’ ]
As you can see Ramda-debug’s field of view function look.fov acts almost the same as R.tap but creates a much nicer output that includes both inputs and outputs. If you wanted to find out the inputs and outputs of any of the inner composed functions like getPeopleUnder65, you would just wrap look.fov around that function instead e.g.
R.pipe(look.fov(getPeopleUnder65), getPeoplesNames)
Ramda-debug has a few other features and settings to control your debugging so I recommend you try it out!
Ramda Vs Lodash
Now you might be thinking. This is old hack, Lodash has provided chaining like… forever.
Let’s look at how we might write our people example using Lodash in an explicit way.
import _ from ‘lodash’;
_.chain(people).filter(person => person.age < 65).map(person => person.name).value();
// => [“John”, “Bob”]
In the Lodash example we use _chain as a wrapper around our object to ‘add functional programmingness’ to it so we can then call filter and map followed by a call to the value() function at the end when we finally want the answer…. What? Is that really functional?
We could also write our people example using Lodash in an implicit way.
import _ from ‘lodash’;
_(people).filter(person => person.age < 65).map(person => person.name);
// => [“John”, “Bob”]
Ok it’s a little bit better…
As you can see Lodash doesn’t feel like a pure functional programming approach. Using the explicit approach Lodash enriches objects so they can be treated in a functional way. One of those enrichment functions is .value(). Using the implicit approach is a little bit better but we still need a wrapper i.e. the underscore _(people).
Recently Lodash has added flow and flowRight which are clones of Ramda’s compose and pipe functions.
To be honest you can use whatever you like, because my goal is to get you thinking about functional programming. However, I much prefer Ramda as it’s just a bunch of functions creating … yes… more functions. There’s no confusion about whether you should use it explicitly or implicitly or use flow/flowRight. Lodash doesn’t really create functions. It tries to extend what ES6 has implemented by adding functions to objects being passed through / processed.
If you make functions first class citizens then you don’t need to add functions to objects. Ramda provides a pure functional programming approach… or at least the best implementation I’ve seen so far because you just create functions and pass data into functions or function chains. That’s the only way you can use it. This pattern is so popular that many other frameworks have emerged doing the same thing i.e to create higher order components like Recompose which I’ll cover in a future post. Ramda really deserves its own post as it has many many more functions that replace common code routines you would normally write by hand, so I highly recommend you check it out.
Summary
When compared to more traditional approaches to processing data, functional programming allows us to:
- describe what we want, not how to get it
- write less code
- write more stable code
- write more performant code
- write more intuitive and powerful code when implemented with Ramda
I hope I’ve convinced you that functional programming (especially with Ramda) is AWESOME and will make you a better programmer. If you’re not sure, I would love to hear your opinion.
Source: https://www.davinryan.com/getting-started-with-functional-programming-and-ramda/
na
Missing the “<65" logic under the The Rise of Ramda section?