Why I'm using `lodash/fp` and you should too

viktorbezdek

Viktor Bezdek

Posted on January 25, 2023

Why I'm using `lodash/fp` and you should too

Are you tired of the same old, clunky way of manipulating data in JavaScript/TypeScript? Say goodbye to bulky, hard-to-read code and hello to a more efficient and elegant solution with lodash/fp. With its focus on immutability and a functional approach, lodash/fp allows you to write clean, testable, and maintainable code that is a joy to work with. Whether you're a seasoned pro or just starting out, lodash/fp is the perfect tool to make your data manipulation tasks a breeze.

lodash is a general-purpose utility library that provides a wide variety of functions for manipulating data. It is not designed specifically for functional programming, and many of its functions have side-effects and perform in-place operations on data.

lodash/fp, on the other hand, is a functional programming library that is built on top of lodash. It provides a curated set of functions that are specifically designed for functional programming and are designed to be immutable and side-effect free.

Key differences of lodash and lodash/fp are

  • Immutable — functions are immutable and side-effect free
  • Data-last — data is always the last argument
  • Autocurried — all functions where it's applicable are curried

What makes lodash/fp awesome

Function composition

Functions are designed to be composed together, which makes it easier to build complex data processing pipelines. The _.flow function allows you to chain multiple functions together and apply them to the input in a single call. Lodash functions do not provide a similar function.

Example

We want to filter the data array to include only people that are older than 30 or have an income greater than 60000, then sort them by their income and age, group them by gender, take the first two elements of each group, reverse them and only keep the name and income properties, then return an array of objects that contains gender and people properties, sort the resulting array by gender, and finally, extract the final value.

Sample data
const data = [
  { name: "Alice", age: 25, gender: "female", income: 50000 },
  { name: "Bob", age: 30, gender: "male", income: 60000 },
  { name: "Charlie", age: 35, gender: "male", income: 70000 },
  { name: "David", age: 40, gender: "male", income: 80000 },
  { name: "Eve", age: 45, gender: "female", income: 90000 },
]
Enter fullscreen mode Exit fullscreen mode
Implementation with lodash/fp
const processData = _.flow(
  _.filter(
    _.overSome([
      _.matches({ age: _.gte(30) }),
      _.matches({ income: _.gte(60000) }),
    ])
  ),
  _.sortBy(["income", "age"]),
  _.groupBy("gender"),
  _.mapValues(_.flow(_.map(_.pick(["name", "income"])), _.take(2), _.reverse)),
  _.toPairs,
  _.map(([gender, people]) => ({ gender, people })),
  _.sortBy("gender"),
  _.value()
)

console.log(processData(data))

/*
[
  { gender: 'female', people: [ { name: 'Eve', income: 90000 } ] },
  { gender: 'male', people: [
    { name: 'David', income: 80000 },
    { name: 'Charlie', income: 70000 }
  ] }
]
*/
Enter fullscreen mode Exit fullscreen mode
Implementation in Vanilla JavaScript
const filteredData = data.filter(
  (person) => person.age >= 30 || person.income >= 60000
)

const sortedData = filteredData.sort((a, b) => {
  if (a.income === b.income) {
    return a.age - b.age
  }
  return a.income - b.income
})

const groupedData = sortedData.reduce((acc, person) => {
  if (!acc[person.gender]) {
    acc[person.gender] = []
  }
  acc[person.gender].push(person)
  return acc
}, {})

const processedData = Object.entries(groupedData).map(([gender, people]) => {
  return {
    gender: gender,
    people: people
      .slice(0, 2)
      .reverse()
      .map((p) => ({ name: p.name, income: p.income })),
  }
})

processedData.sort((a, b) => a.gender.localeCompare(b.gender))

console.log(processedData)
/*
[
  { gender: 'female', people: [ { name: 'Eve', income: 90000 } ] },
  { gender: 'male', people: [
    { name: 'David', income: 80000 },
    { name: 'Charlie', income: 70000 }
  ] }
]
*/
Enter fullscreen mode Exit fullscreen mode

The code is less concise, less readable, and it's harder to understand the order of operations, but it still achieves the same result. Now let's imagine writing tests for both implementations. You see where I'm going, right? :)

It's worth noting that functional composition can also be achieved in vanilla javascript using by writing your own compose function or with other libraries like ramda. I personally prefer lodash/fp as it's somewhat standard within it's category.

Performance

Functions are optimized for performance and are highly tested and well-maintained, this makes them more reliable and efficient than some of the native javascript methods. Note: this applies to lodash as well. I ran a few naive benchmarks to support this statement. Each result is mean of 100 runs.

  • lodashFP.each vs. lodash.each vs. native forEach
    • Native: 553.345791ms
    • Lodash/fp: 453.379625ms
    • Lodash: 439.157208ms
  • lodashFP.filter vs. lodash.filter vs. native filter
    • Native: 858.684666ms
    • Lodash/fp: 698.602ms
    • Lodash: 690.892667ms
  • lodashFP.find vs. lodash.find vs. native find
    • Native: 583.027ms
    • Lodash/fp: 130.7045ms
    • Lodash: 128.702625ms
  • lodashFP.map vs. lodash.map vs. native map
    • Native: 783.693083ms
    • Lodash/fp: 213.2525ms
    • Lodash: 220.822916ms

Results will show that lodash will do the iterations significantly faster compared to native functions. This is because lodash implementations are using for loop internally which is faster than native array iterators in some cases because native array iterators has to call the callback function for each element, and each call creates a new stack frame, which can be slow for large arrays.

Plethora really useful helper functions

lodash/fp (as well as standard lodash) is feature complete toolbox for any transforming, processing, traversing task you might face. Investing the effort into learning how to use it is extremely rewarding.

I've put together few practical examples to show how it can help you deliver with less effort.

Grouping, calulations

Calculate the average income of all people in the data array grouped by gender isn't very complicated task to do, but function composition and few helper methods can make it one liner (with ugly formatting).

const data = [
  { name: "Alice", age: 25, gender: "female", income: 50000 },
  { name: "Bob", age: 30, gender: "male", income: 60000 },
  { name: "Charlie", age: 35, gender: "male", income: 70000 },
  { name: "David", age: 40, gender: "male", income: 80000 },
  { name: "Eve", age: 45, gender: "female", income: 90000 },
]

const averageIncomeByGender = _.flow(
  _.groupBy("gender"),
  _.mapValues(_.flow(_.map("income"), _.mean))
)
console.log(averageIncomeByGender(data))
// { female: 75000, male: 65000 }
Enter fullscreen mode Exit fullscreen mode

First, we use _.groupBy('gender') to group all people by gender, then we use _.mapValues to iterate over each group and apply a transformation function to it's value.

The transformation function is defined with _.flow(_.map('income'), _.mean) where we first extract the income property of each person in the group with _.map('income') and then calculate the mean income of the group with _.mean

Error handling

Mighty _.cond function have many use-cases. Here's how we can use it to handle various error scenarios.

const divide = (a, b) => {
  const div = _.cond([
    [
      _.isNaN(a),
      () => {
        throw new Error("a must be a number")
      },
    ],
    [
      _.isNaN(b),
      () => {
        throw new Error("b must be a number")
      },
    ],
    [
      _.eq(b, 0),
      () => {
        throw new Error("cannot divide by zero")
      },
    ],
    [_.T, () => a / b],
  ])
  return div()
}
console.log(divide(10, 5)) // 2
console.log(divide(10, "a")) // throws an error: "b must be a number"
console.log(divide(10, 0)) // throws an error: "cannot divide by zero"
Enter fullscreen mode Exit fullscreen mode

The _.cond function takes an array of condition-callback pairs and returns a new function. When the returned function is called, it will evaluate the conditions in the order they were provided, and call the first callback whose corresponding condition returns a truthy value. If none of the conditions are true, it will call the last callback with the arguments _.T.

I recently learned that pattern matching might be coming to JavaScript/TypeScript natively.

Filtering

In this example, I used the _.cond function along with other helper functions like _.filter, _.flow, _.prop, _.gt, _.eq, _.lt to perform complex filtering on the array of data. The goal of this example is to filter books which are either longer than 300 pages and title starts with "Harry" or books released before 1940 in Fiction genre.

const books = [
  {
    title: "Harry Potter and the Sorcerer's Stone",
    author: "J.K. Rowling",
    pages: 301,
    genre: "Fantasy",
    publicationYear: 1997,
  },
  {
    title: "The Great Gatsby",
    author: "F. Scott Fitzgerald",
    pages: 180,
    genre: "Fiction",
    publicationYear: 1925,
  },
  {
    title: "The Catcher in the Rye",
    author: "J.D. Salinger",
    pages: 277,
    genre: "Fiction",
    publicationYear: 1951,
  },
  {
    title: "The Lord of the Rings",
    author: "J.R.R. Tolkien",
    pages: 1178,
    genre: "Fantasy",
    publicationYear: 1954,
  },
  {
    title: "The Hobbit",
    author: "J.R.R. Tolkien",
    pages: 290,
    genre: "Fantasy",
    publicationYear: 1937,
  },
]

// Using cond and other helper functions to filter books based on multiple conditions
const filteredBooks = _.filter(
  _.cond([
    [
      _.flow(_.prop("pages"), _.lt(300)),
      _.flow(_.prop('title'), _.startsWith('Harry')),
    ],
    [
      _.flow(_.prop("publicationYear"), _.gt(1940)),
      _.flow(_.prop("genre"), _.eq("Fiction")),
    ],
  ])
)(books)

console.log(filteredBooks)

/*
[
  {
    title: "Harry Potter and the Sorcerer's Stone",
    author: 'J.K. Rowling',
    pages: 301,
    genre: 'Fantasy',
    publicationYear: 1997
  },
  {
    title: 'The Great Gatsby',
    author: 'F. Scott Fitzgerald',
    pages: 180,
    genre: 'Fiction',
    publicationYear: 1925
  }
]
*/
Enter fullscreen mode Exit fullscreen mode

In conclusion, lodash/fp offers a wide range of functional programming utilities that can greatly enhance the readability and maintainability of your code. From its powerful collection manipulation methods to its functional composition capabilities, lodash/fp can help you write cleaner and more efficient code. Additionally, its use of immutable data structures and its adherence to functional programming principles make it a great choice for projects that prioritize performance and scalability. Give lodash/fp a try and see how it can improve your development workflow.

💖 💪 🙅 🚩
viktorbezdek
Viktor Bezdek

Posted on January 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related