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
- We'll use codepen as our playground, navigate to this page:
-
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: '[email protected]',
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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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:
Nice gist!
In this part:
I think there's a great opportunity to showcase how auto-currying works, by doing this instead:
And maybe talk about point-free style. (Look ma, no booking!)