Skip to content

Instantly share code, notes, and snippets.

@brauliodiez
Last active September 1, 2023 04:48
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save brauliodiez/9fd567fdc8b2695c11a61daa50475f43 to your computer and use it in GitHub Desktop.
Save brauliodiez/9fd567fdc8b2695c11a61daa50475f43 to your computer and use it in GitHub Desktop.
lodash/fp set and flow

lodash/fp - set / flow

In this gist we are going to learn about basic goodies that we get from the library lodash/fp (fp stands for functional programming, great for ensuring immutability).We'll learn just by doing (step by step guided sample + codepens).

We'll cover lodash set and flow functions

Steps

  • We'll use codepen as our playground, navigate to this page:

https://codepen.io/

  • Click on create >> new pen (a new blank pen will be created for you to play with).

  • Import lodash/fp from the CDN: go to settings, click on the javascript tags and on Add Extrernal Javascript, include this CDN link:

https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)

If we are working locally, we can npm install lodash and then import it using something like import {flow, set} from 'lodash/fp';

  • In this sample, we want to manage a client hotel booking, let's suposse we are working with a complex entity that contains nested objects, let paste this entity (for the sake of this sample, this is a simplified entity):
const myBooking = {
  id: '23484',
  client: {
      id: '458982',
      name: 'John Doe',
      email: 'john.doe@test.com',
      vip: false,
      business: false,
      family: false,
  }, 
  rooms: [
     {
       type: 'standard',
       beds: 2,       
     },
     {
       type: 'standard',
       beds: 1,       
     }
  ],
  extras: {
     lateCheckout: false,
     spa: false,
     minibar: false,
     seasights: false,
  }
}
  • Now let's assume another developer has build a smart system based on client reputation, it will offer some extras at no cost, he will provide us with a list of settings to toggle (upgrade), some dynamic patterns could be "client.vip", or ["lateCheckout", "spa"] or even "room[0].type" and on the other hand we want to keep our original booking object immutable, what can we do? split / loops / parsing...? lodash to the rescue !

  • Let's start by creating a booking upgrade method, here we'll dynamically receive the property and value that requires to be updated and using lodash set we'll just traverse through the properties, set the right value and return a new object including the changes. Let's add the following code in our code pen:

const upgradeBooking = (propertypath, value, booking) => {
   return _.set(propertypath, value, booking);
}

Link to the lodash official documentation (set), this is the mutable version, fp(immutable) version move the "object" value to the last position of the params list instead of the first.

  • We are going to give a try to the function:
const updatedBookingA = upgradeBooking('client.vip', true, myBooking);

console.log('##### Original Booking (does not mutate) #####');
console.log(myBooking);
console.log('#########');
console.log('##### Updated Booking #####');
console.log(updatedBookingA);
console.log('#########');

The result that we get dumped into the console:

"##### Original Booking (does not mutate) #####"

Object {
  client: Object {
    business: false,
    email: "john.doe@test.com",
    family: false,
    id: "458982",
    name: "John Doe",
    vip: false
  },
  extras: Object {
    lateCheckout: false,
    minibar: false,
    seasights: false,
    spa: false
  },
  id: "23484",
  rooms: [Object {
  beds: 2,
  type: "standard"
}, Object {
  beds: 1,
  type: "standard"
}]
}
"#########"
"##### Updated Booking #####"

Object {
  client: Object {
    business: false,
    email: "john.doe@test.com",
    family: false,
    id: "458982",
    name: "John Doe",
+    vip: true
  },
  extras: Object {
    lateCheckout: false,
    minibar: false,
    seasights: false,
    spa: false
  },
  id: "23484",
  rooms: [Object {
  beds: 2,
  type: "standard"
}, Object {
  beds: 1,
  type: "standard"
}]
}
"#########"
  • It's time to test the sample (click on Run button, or if you have Auto-Updating the sample will already be run). In the console we can see the original object and the updated one.

  • What if the "smart system" recommends us to upgrade the first room booked to a superior one? do we need to update or upgradeClient function to handle the array? The answer is no, we can do it by calling:

const updatedBookingB = upgradeBooking('rooms[0].type', 'suite', updatedBookingA);
console.log(updatedBookingB);
  • Let's say on the extras side we only get the name of the extra property but not the full path, what could we do to get the full path? string interpolation to the rescue:
const extraToUpgrade = 'lateCheckout';
const updatedBookingC = upgradeBooking(`extras.${extraToUpgrade}`, true, updatedBookingB);
console.log(updatedBookingC);

Another option could be to use the param array approach implementation from set.

const updatedBookingC = upgradeBooking(['extras',extraToUpgrade], true, updatedBookingB);

The final object that we get after applying the transformations:

Object {
  client: Object {
    business: false,
    email: "john.doe@test.com",
    family: false,
    id: "458982",
    name: "John Doe",
+    vip: true
  },
  extras: Object {
+    lateCheckout: true,
    minibar: false,
    seasights: false,
    spa: false
  },
  id: "23484",
  rooms: [Object {
  beds: 2,
+  type: "suite"
}, Object {
  beds: 1,
  type: "standard"
}]
}

Want to give a try? Check the working codepen sample

  • Performing this dynamic updates and in an immutable manner is great, but I wouldn't like to be creating a BookingA, BookingB, BookingC... objects, is there any easy wat to chain / compose this The answer is yes, lodash flow does that job for you, we could simplify all the previous work by doing:
const extraToUpgrade = 'lateCheckout';

const finalBooking = _.flow(
  _.set('client.vip', true),
  _.set('rooms[0].type', 'suite'),
  _.set(`extras.${extraToUpgrade}`, true),
)(myBooking);

console.log(finalBooking);

About flow: What this does under the hood: it invokes the first function passing myBooking as last parameter (applying currying), then each successive invocation is supplied the return value of the previous.

Want to give a try? Check the working codepen sample

  • That's awesome, but I would like to keep my 'upgradeBooking' function that I have created before, what can I do? In order to do this we need to currify our upgradeClient function (more info about currying plus sample).
- const upgradeBooking = (propertypath, value, booking) => {
+ const upgradeBoooking = (propertypath, value) => (booking) => {
   return _.set(propertypath, value, booking);
}
  • Now we can use it inside lodash flow:
const finalBooking = _.flow(
  upgradeBooking('client.vip', true),
  upgradeBooking('rooms[0].type', 'suite'),
  upgradeBooking(`extras.${extraToUpgrade}`, true),
)(myBooking);

console.log(finalBooking);

We could use as well lodash curry helper and keep our function curry free.

const upgradeBooking = _.curry((propertypath, value, booking) => {
   return _.set(propertypath, value, booking);
});


const finalBooking = _.flow(
  upgradeBooking('client.vip', true),
  upgradeBooking('rooms[0].type', 'suite'),
  upgradeBooking(`extras.${extraToUpgrade}`, true),
)(myBooking);

Want to give a try? Check the working codepen for this sample

  • So far so good, that was cool, but as a colleague says code is read more than is written, it would be a good idea to group all this "magic" into something more human readable, to help the next developer that will jump into this code (or yourself in two months time ;)) on understanding what is this code doing. What do you think about this approach?

Creating some helper functions (adding semanthic)

const upgradeBasic = (key, value) => (booking) =>
  upgradeBooking(`client.${key}`, value)(booking);

const upgradeRoom = (key, value) => (booking) =>
  upgradeBooking(`rooms[0].${key}`, value)(booking);

const upgradeExtras = (key, value) => (booking) =>
  upgradeBooking(`extras.${key}`, value)(booking);
  • Now the sequence inside flow it's more readable:
const finalBooking = _.flow(
  upgradeBasic('vip', true),
  upgradeRoom('type', 'suite'),
  upgradeExtras(extraToUpgrade, true),
)(myBooking);

console.log(finalBooking);

Want to give a try? Check the working codepen for this sample

Hope you have enjoyed this gist, I have compiled the working sample in several codepens:

@MinimumViablePerson
Copy link

MinimumViablePerson commented Sep 30, 2019

Nice gist!

In this part:

- const upgradeBooking = (propertypath, value, booking) => {
+ const upgradeBooking = (propertypath, value) => (booking) => {
   return _.set(propertypath, value, booking);
}

I think there's a great opportunity to showcase how auto-currying works, by doing this instead:

- const upgradeBooking = (propertypath, value, booking) => {
+ const upgradeBooking = (propertypath, value) =>  _.set(propertypath, value)

And maybe talk about point-free style. (Look ma, no booking!)

@abduwaly
Copy link

abduwaly commented Jan 21, 2020

Hi @brauliodiez,
Just looking into Immer in order to introduce a way to handle the state immutably inside my reducer, I'm wondering if you have any recommendation about that ? Or what do you think the advantages of lodash way to do that ? (compared to libraries like Immer, Immutable-js ...)

@brauliodiez
Copy link
Author

brauliodiez commented Jan 23, 2020 via email

@ssirowy
Copy link

ssirowy commented Aug 13, 2020

Nice gist!

In this part:

- const upgradeBooking = (propertypath, value, booking) => {
+ const upgradeBooking = (propertypath, value) => (booking) => {
   return _.set(propertypath, value, booking);
}

I think there's a great opportunity to showcase how auto-currying works, by doing this instead:

- const upgradeBooking = (propertypath, value, booking) => {
+ const upgradeBooking = (propertypath, value) =>  _.set(propertypath, value)

And maybe talk about point-free style. (Look ma, no booking!)

Although this still isnt point-free since you still have "points" for propertyPath and value

You can consider going one step further here and recognize that the upgradeBooking and set functions both have the exact same signatures, and that upgradeBooking is really just acting as a thin proxy for set, in which we can model like this:

- const upgradeBooking = (propertypath, value, booking) => {
- const upgradeBooking = (propertypath, value) =>  _.set(propertypath, value)

+ const upgradeBooking = _.set

Now point-free!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment