Why I'm using `lodash/fp` and you should too
Viktor Bezdek
Posted on January 25, 2023
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 },
]
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 }
] }
]
*/
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 }
] }
]
*/
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. nativeforEach
- Native: 553.345791ms
- Lodash/fp: 453.379625ms
- Lodash: 439.157208ms
-
lodashFP.filter
vs.lodash.filter
vs. nativefilter
- Native: 858.684666ms
- Lodash/fp: 698.602ms
- Lodash: 690.892667ms
-
lodashFP.find
vs.lodash.find
vs. nativefind
- Native: 583.027ms
- Lodash/fp: 130.7045ms
- Lodash: 128.702625ms
-
lodashFP.map
vs.lodash.map
vs. nativemap
- 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 }
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"
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
}
]
*/
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.
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
November 21, 2024