Functional Programming in JavaScript and how to do it with Ramda

Juan SalasJuan Salas

Juan Salas

Ramda is a JavaScript library that makes functional programming simple. While JavaScript libraries with functional capabilities have been around for a while, Ramda acts as a complete standard library specifically catered to the functional paradigm.

Ramda is a JavaScript library that makes functional programming simple. While JavaScript libraries with functional capabilities have been around for awhile, Ramda acts as a complete standard library specifically catered to the functional paradigm. Unlike other languages such as C++, Python, Java, etc. JavaScript doesn’t have a great standard library. Oftentimes, you have to install external libraries to get functionality that is built into other languages. But, Ramda can fill that gap nicely as it provides many functions for manipulating data, grouping, filtering, etc.

To understand what really makes Ramda different though, let’s first take a look at some functional programming concepts.

Overview of Functional Programming

The Basics

If you’re new to functional programming or primarily use object-oriented programming, learning this new paradigm might seem daunting. It’s mathematical definitions and academia’s affinity for it tend to scare off some newcomers, and there is a bit of a learning curve to wrap your head around. But functional programming has a number of advantages that make it very worthwhile to learn.

At its core, functional programming emphasizes pure functions. Pure functions basically have two components:

1. Given the same arguments, a function will always return the same result.

2. It has no side effects.

As an example of the first property, say you are building a program to sell cupcakes. Your cupcakes cost $2 each. Now, you want to calculate the order total for someone buying multiple cupcakes. You might write something like this:

let PRICE = 2;
const orderTotal = (numCupcakes) => numCupcakes * PRICE;
orderTotal(5) // returns 10

Now, let’s say you change the price per cupcake and make them $3. The customer still orders $5. But this means our orderTotal method now returns 15, despite being given the same arguments. This means the function is impure. But, we can easily fix this.

const orderTotal = (price, numCupcakes) => numCupcakes * price;let PRICE = 2;
orderTotal(PRICE, 5) // returns 10

Now, given the same arguments, this function will always return the same result.

Let’s demonstrate the second example. If you use object-oriented programming, you’re probably familiar with mutation and changing state — but functional programming discourages this. For example, let’s say we want to discount our cupcakes.

let PRICE = 2;
const discount = (percentOff) => PRICE = PRICE - (PRICE * (percentOff/100));
discount(10); // returns 1.8
console.log(PRICE); // returns 1.8

Our PRICE has now changed! We don’t want that, so let’s fix it.

const discount = (percentOff, price) => price - (price * (percentOff/100));let PRICE = 2;
discount(10, PRICE); // returns 1.8
console.log(PRICE); // returns 2

Adding the price to our list of arguments means we aren’t modifying PRICE, and our function is pure.

These two properties together ensure that everything is deterministic and your functions always return reliable results — essentially, you’re working with mathematical formulas. Inputting arguments in a formula should give consistent results and should not change how the other formulas work. 2+2 will always return 4, just as add(2, 2) should always return 4.

Now that we have a basic definition of functional programming, let’s take a look at some common techniques used in this paradigm. This will give us a better idea of how Ramda works and why it’s unique.

Currying

Currying takes a function with n arguments and turns into into n functions, each with 1 argument. This allows you to create smaller, reusable pieces of code more easily. Let’s take a look at how this works. We’ll turn our discount function above into a curried function:

function discount (percentOff) {
  return function (price) {
    return price - price (percentOFf/100))
  }
}
// OR a simpler way to write it

const discount = (percentOff) => (price) => price-(price *(percentOff/100))

let PRICE = 2
discount(10, PRICE) // returns a function

Calling discount as we did before will return a function. That function, when called, will return a number. It’s basically dropping the second argument, because it has nothing to do with it. It’s the same as calling it with one argument. To call our discount function now, we need to write it like this:

discount(10)(PRICE) // returns 1.8

Let’s say we have a bunch of different items with different prices, and we want to be able to give a ten percent discount to any of them. Well, now we can use our discount function to do this:

const tenPercentDiscount = discount(10)
tenPercentDiscount(PRICE) // returns 1.8

We’ve used a more general function, discount, to easily create the tenPercentDiscount function. We did this using both currying and something called partial application. If a curried function can take up to n arguments, then partially applying the function means calling it with n-1 or fewer arguments. Essentially, you only use part of the function, and the result will be a function that accepts the rest of the arguments.

This also takes us to the idea of functional composition. Functional composition lets us combine two or more functions to create a new one. As an example, let’s say we’re giving a ten percent discount on items in the store, and a customer has a coupon for an additional ten cents off (we’re not very generous). We can do this:

const tenPercentDiscount = discount(10)
const tenCentsOff = (price) => price - (10/100)
const tenPercentAndTenCentsOff = tenCentsOff(tenPercentDiscount(PRICE))// returns 1.7

What we’re doing here is taking several smaller functions and using them to build a more complicated one. It’s important to note the order of evaluation here, or the pipeline we’ve created. tenPercentDiscount(PRICE) will evaluate first, and the result of that will be passed into tenCentsOff. Make sure when you’re composing functions, you have them in the order in which they should be evaluated.

Now that we know some functional programming concepts, let’s look at Ramda.

Ramda

One important thing to know about Ramda is that functions are automatically curried. This means you can easily compose new functions with very little code. Let’s see how this works with our previous example of adding a ten percent discount and removing ten cents from the price.

(Note: if you want to try the code out yourself, you can do so here)

const tenPercentAndTenCentsOff = R.compose(tenCentsOff, tenPercentDiscount)
tenPercentAndTenCentsOff(PRICE) // returns 1.7

Let’s say we have a number of baked goods we want to apply this discount to. We can do this very easily with a list of prices:

let PRICES = [2, 3, 1]
R.map(tenPercentAndTenCentsOff, PRICES) // returns [1.7, 2.6, 0.8]

But, it doesn’t really make sense to just have a list of prices. What goods do they correspond to? We can put them in an object, but then our code gets more complicated because we need to call map on just the prices.

Well, the fun thing about Ramda is many of the functions are polymorphic, meaning they can take different types of arguments. While JavaScript’s built in map works on arrays, Ramda’s map works on arrays and objects.

So, we just turn our PRICES into an object.

let PRICES = {cupcake: 2, cake: 3, cookie: 1}
R.map(tenPercentAndTenCentsOff, PRICES) // returns {"cake": 2.6, "cookie": 0.8, "cupcake": 1.7}

Of course this is a fairly simple example, but hopefully it shows you how Ramda lets you build up complicated logic very easily to work with data.

Functions and Examples

The number of functions in the library can be intimidating, so let’s take a look at some of the most used Ramda functions using the bakery example.

filter

Let’s say we want to find which of our items are less than a dollar after discounts are applied. We can use filter to do this:

let PRICES = {cupcake: 2, cake: 3, cookie: 1}
const lessThanADollar = (a) => a < 1
const discountedItems = R.map(tenPercentAndTenCentsOff, PRICES)
R.filter(lessThanADollar, discountedItems) // returns {"cookie": 0.8}

pipe

We can use pipe to easily perform calculations from left to right. Per the documentation, the first function given can have any number of arguments, but the functions following it must only take one argument.

So let’s say everything in our store is 50% off and a customer has a ten percent discount. We can use pipe and the previous functions we built to calculate this for everything in the store.

const newPrices = R.pipe(R.multiply(.5), tenPercentDiscount)
R.map(newPrices, PRICES) //returns {"cake": 1.35, "cookie": 0.45, "cupcake": 0.9}

zip

If we need to combine two lists together, we can use zip. So if we hadn’t made an object with the items and prices, and instead had them in two separate lists, we could combine them like so:

const ITEMS = ["cupcake", "cake", "cookie"]
const PRICES = [2, 3, 1]
R.zip(ITEMS, PRICES) // returns [["cupcake", 2], ["cake", 3], ["cookie", 1]]

uniqWith

Let’s say we messed up our list of prices, and now there’s a bunch of duplicates. We want to simply know the unique prices of the items we sell. We can use uniqWith to give us a list with only one copy of each element.

const PRICES = ["2", "3", 1, 2, 3, 1, 2, 2, 1, 1, "1", "3"]
const strEq = R.eqBy(String);
R.uniqWith(strEq)(PRICES) // returns ["2", "3", 1]

sum

Now that we’ve de-dupped our prices list, a customer has decided to order one of everything and we need to add together all of the prices. We can use sum to do this easily, and we don’t even have to worry about the two strings in the array.

const DUP_PRICES = ["2", "3", 1, 2, 3, 1, 2, 2, 1, 1, "1", "3"]
const strEq = R.eqBy(String);
const ORIGINAL_PRICES = R.uniqWith(strEq)(DUP_PRICES) // returns ["2", "3", 1]
R.sum(ORIGINAL_PRICES) // returns 6

groupBy

Let’s say our bakery has been doing really well, and we’ve added a number of new items to our menu. We now have multiple types of cookies, cakes, and cupcakes and need a way to sort our menu items. Let’s use groupBy to do just that.

const ITEMS = [
 {type: 'cookie', name: 'almond', price: '3'},
 {type: 'cake', name: 'red velvet', price: '5'},
 {type: 'cupcake', name: 'chocolate', price: '2'},
 {type: 'cake', name: 'black forest gateau', price: '6'},
 {type: 'cookie', name: 'choclate chip', price: '2'},
 {type: 'cupcake', name: 'vanilla', price: '1'}
]

const groupByType = R.groupBy((item) => item.type)
groupByType(ITEMS)
/** returns
{
cake: [
    {
        name: "red velvet",
        price: "5",
        type: "cake"
    },
    {
        name: "black forest gateau",
        price: "6",
        type: "cake"
    }
],
cookie: [
    {
        name: "almond",
        price: "3",
        type: "cookie"
    },...etc
**/

omit

Now we have our menu items, but we’ve sold out of cookies for now. So let’s use omit to get the rest of the items we do have.

const groupedItems = groupByType(ITEMS)
R.omit(['cookie'], groupedItems)
/** returns
{
cake: [
    {
        name: "red velvet",
        price: "5",
        type: "cake"
    },
    {
        name: "black forest gateau",
        price: "6",
        type: "cake"
    }
],
cupcake: [
    {
        name: "chocolate",
        price: "2",
        type: "cupcake"
    },
    {
        name: "vanilla",
        price: "1",
        type: "cupcake"
    }
]
}
**/

propEq

Now we want to find everything on our full menu that’s priced at $2. We can do that easily with propEq.

const isTwoDollars = R.propEq('price', '2')
R.filter(isTwoDollars, ITEMS)
/** returns
[
{
    name: "chocolate",
    price: "2",
    type: "cupcake"
},
{
    name: "choclate chip",
    price: "2",
    type: "cookie"
}
]
**/

range

Finally, we have a customer call who wants to know what items we have available in their price range. We can easily find items on our menu that fit this range using a combination of filter and range, like this:

const ITEMS = [
 {type: 'cookie', name: 'almond', price: '3'},
 {type: 'cake', name: 'red velvet', price: '5'},
 {type: 'cupcake', name: 'chocolate', price: '2'},
 {type: 'cake', name: 'black forest gateau', price: '6'},
 {type: 'cookie', name: 'choclate chip', price: '2'},
 {type: 'cupcake', name: 'vanilla', price: '1'}
]

const isInPriceRange = R.curry(function (priceRange, item) {
 let last = priceRange[priceRange.length - 1]
 return item.price > priceRange[0] && item.price <= priceRange[last]
})

const PRICE_RANGE = R.range(0, 4)
R.filter(isInPriceRange(PRICE_RANGE), ITEMS)

/** returns
[
{
    name: "almond",
    price: "3",
    type: "cookie"
},
{
    name: "chocolate",
    price: "2",
    type: "cupcake"
},
{
    name: "choclate chip",
    price: "2",
    type: "cookie"
},
{
    name: "vanilla",
    price: "1",
    type: "cupcake"
}
]
**/

Conclusion

Ramda is a powerful, well-tested and well-documented library for functional programming in JavaScript. Since it gives us access to many useful functions that we can use for a huge range of purposes, it can serve as a “standard library.” By currying functions by default, it allows us to easily compose functions and gives us predictable and testable code. Since it allows you to pick and choose the functions you need from it, it’s an incredibly useful tool for using in any JavaScript project.

Get to know Celerative's JavaScript experts