Chaining and function composition with lodash / underscore

nicolascarlo1 672 views 38 slides Mar 15, 2016
Slide 1
Slide 1 of 38
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38

About This Presentation

Chaining and function composition with lodash / underscore. Talk presented on June 23rd, 2015 at Backbone.js Paris S01E07 meetup.


Slide Content

FUNCTIONS FUNCTIONS CHAININGCHAINING AND AND
COMPOSITIONCOMPOSITION WITH _ WITH _

@NICOESPEON@NICOESPEON
http://nicoespeon.com

BEFORE WE BEFORE WE DIVE INDIVE IN
> A QUICK CHECK-UP> A QUICK CHECK-UP

WHEN I SAY WHEN I SAY __
I personally use (v3.0+)
Works with too — for the most part \o/
lodash
underscore

FUNCTIONAL PROGRAMMING?FUNCTIONAL PROGRAMMING?
First-class functions
Higher order functions
Pure functions
Recursion
Immutability
≠ imperative programming
but !≠ OOP

FUNCTIONS FUNCTIONS CHAININGCHAINING
> WHAT IS THAT FOR?> WHAT IS THAT FOR?

A CONCRETE CASE : MULTIPLE OPERATIONS (0)A CONCRETE CASE : MULTIPLE OPERATIONS (0)
this.set( "items", [
{ id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 },
null,
{ id: "ebaaa82e", cepage: "gamay", type: "grape", quantity: 2 },
{ id: "ee2bcc12", cepage: "viognier", type: "grape", quantity: 0 }
] );

A CONCRETE CASE : MULTIPLE OPERATIONS (1)A CONCRETE CASE : MULTIPLE OPERATIONS (1)
function getItems() {
return _.compact( this.get( "items" ) );
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 },
// { id: "ebaaa82e", cepage: "gamay", type: "grape", quantity: 2 },
// { id: "ee2bcc12", cepage: "viognier", type: "grape", quantity: 0 }
// ]

A CONCRETE CASE : MULTIPLE OPERATIONS (2)A CONCRETE CASE : MULTIPLE OPERATIONS (2)
function getItems() {
return _.reject( _.compact( this.get( "items" ) ), { quantity: 0 } );
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 },
// { id: "ebaaa82e", cepage: "gamay", type: "grape", quantity: 2 }
// ]

A CONCRETE CASE : MULTIPLE OPERATIONS (3)A CONCRETE CASE : MULTIPLE OPERATIONS (3)
function getItems() {
return _.filter(
_.reject(
_.compact( this.get( "items" ) ),
{ quantity: 0 }
),
{ type: "juice" }
);
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 }
// ]

A CONCRETE CASE : MULTIPLE OPERATIONS (3)A CONCRETE CASE : MULTIPLE OPERATIONS (3)
// Better, really?
function getItems() {
var compactedItems = _.compact( this.get( "items" ) );
var positiveCompactedItems = _.reject( compactedItems, { quantity: 0 } );
return _.filter( positiveCompactedItems, { type: "juice" } );
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 }
// ]

_.CHAIN(_.CHAIN( VALUE VALUE ))
Creates a lodash object that wraps value with explicit
method chaining enabled.
_.chain( this.get( "items" ) );
// => returns `LodashWrapper`
_.chain( this.get( "items" ) ).compact();
// <=> `_.compact( this.get( "items" ) );`
// BUT… returns `LodashWrapper` too!
// And so we can do ->
_.chain( this.get( "items" ) )
.compact()
.reject( { quantity: 0 } )
.filter( { type: "juice" } )
// …
.map( doSomething );

_.CHAIN(_.CHAIN( VALUE VALUE )) … … .VALUE().VALUE() ! !
Executes the chained sequence to extract the
unwrapped value.
_.chain( this.get( "items" ) )
.compact()
.reject( { quantity: 0 } )
.filter( { type: "juice" } )
// Hum… still return `LodashWrapper` >_<
.value();
// And voilà \o/
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 }
// ]

A CONCRETE CASE : MULTIPLE OPERATIONS (4)A CONCRETE CASE : MULTIPLE OPERATIONS (4)
function getItems() {
return _.chain( this.get( "items" ) )
.compact()
.reject( { quantity: 0 } )
.filter( { type: "juice" } )
.value();
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 }
// ]
function getItems() {
return _( this.get( "items" ) ) // _( value ) === _.chain( value )
.compact()
.reject( { quantity: 0 } )
.filter( { type: "juice" } )
.value();
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 }
// ]

WHAT WAS ALL THE WHAT WAS ALL THE FUSSFUSS ABOUT? ABOUT?
> ADVANTAGES OF CHAINING> ADVANTAGES OF CHAINING

PIPELINE / FLOWPIPELINE / FLOW
function getItems() {
return _( this.get( "items" ) )
.compact()
.reject( isEmpty )
.filter( isJuice )
.map( parseText )
// … we construct the pipeline
// flow is clear, readable!
.value();
}
Does that ring a bell?

PIPELINE / FLOWPIPELINE / FLOW
function makeItemAvailable( userID, index ) {
return _findOneItem( userID, index )
.then( doSomethingClever )
.then( updateStatusAs( "available" ) )
.then( res.ok )
.catch( res.serverError );
}
// You get the same idea with promises.

function getBottles( options ) {
// Ensure default options.
options = _.defaults( {}, options, { isAppellationOnly: false } );
var bottlesWrapper = _( this.get( "bottles" ) ).map( parseText );
// …
// Dynamically build the pipeline.
if( options.isAppellationOnly ) {
bottlesWrapper = bottlesWrapper.pick( [ "appellation" ] );
}
// Nothing have been computed so far!

return bottlesWrapper.value(); // evaluates when needed only!
}
function getParsedBottlesWrapper () {
return _( this.get( "bottles" ) ).map( parseText );
}
function getBottles( options ) {
// Ensure default options.
options = _.defaults( {}, options, { isAppellationOnly: false } );
var bottlesWrapper = getParsedBottlesWrapper.call( this );
// Dynamically build the pipeline.
if( options.isAppellationOnly ) {
bottlesWrapper = bottlesWrapper.pick( [ "appellation" ] );
}
// Nothing have been computed so far!

return bottlesWrapper.value(); // evaluates when needed only!
}
LAZYLAZY EVALUATION EVALUATION

LAZYLAZY EVALUATION EVALUATION
To learn more about this >
http://filimanjaro.com/blog/2014/introducing-lazy-evaluation/

COMPOSITIONCOMPOSITION AND OTHER TRICKS AND OTHER TRICKS
> TO BUILD > TO BUILD EFFICIENTEFFICIENT PIPELINES PIPELINES

COMPOSITION ?COMPOSITION ?
(f⋅g)(x)=f(g(x))
function add10( value ) { // f
return 10 + value;
}
function times3( value ) { // g
return 3 * value;
}
add10( times3( 10 ) ); // (f ∘ g)( 10 )
// => 10 + ( 3 * 10 )
// => 40

COMPOSITION ?COMPOSITION ?
function add10( value ) { // f
return 10 + value;
}
function times3( value ) { // g
return 3 * value;
}
var times3AndAdd10 = _.compose( add10, times3 ); // f ∘ g
times3AndAdd10( 10 );
// => 40
times3AndAdd10( 0 );
// => 10
function add10( value ) { // f
return 10 + value;
}
function times3( value ) { // g
return 3 * value;
}
var times3AndAdd10 = _.flowRight( add10, times3 ); // f ∘ g
times3AndAdd10( 10 );
// => 40
times3AndAdd10( 0 );
// => 10

_.FLOWRIGHT(_.FLOWRIGHT( [FUNCS] [FUNCS] ))
Create a function that will return the result of every
funcs invoked with the result of the preceding function,
from the right to the left (= compose).

_.FLOW(_.FLOW( [FUNCS] [FUNCS] ))
function add10( value ) { // f
return 10 + value;
}
function times3( value ) { // g
return 3 * value;
}
var times3AndAdd10 = _.flow( times3, add10 ); // f ∘ g
times3AndAdd10( 10 );
// => 40
times3AndAdd10( 0 );
// => 10
If _.flowRight is not of your taste.

PARTIAL APPLICATION: PARTIAL APPLICATION: _.PARTIAL()_.PARTIAL()
function greet( greeting, name ) {
return greeting + " " + name;
}
var sayHelloTo = _.partial( greet, "Hello" );
// returns a function with params partially set.
sayHelloTo( "Backbone" );
// → "Hello Backbone"

PARTIAL APPLICATIONPARTIAL APPLICATION
function _isCepageInRecipe( cepage, bottle ) { … }
function _areBuildingsPartOfRecipe ( buildings, bottle ) { … }
function hasMatchingBottles( cepage, buildings ) {
var isCepageInRecipe = _.partial( _isCepageInRecipe, cepage );
var areBuildingsPartOfRecipe = _.partial( _areBuildingsPartOfRecipe, buildings );
return _( this.get( "bottles" ) )
.filter( isCepageInRecipe )
.any( areBuildingsPartOfRecipe );
}
So you can chain in real life…

function greet( greeting, name ) {
return greeting + " " + name;
}
var greetBackbone = _.partialRight( greet, "Backbone" );
// returns a function with params partially set.
greetBackbone( "Hello" );
// → "Hello Backbone"
_.PARTIALRIGHT()_.PARTIALRIGHT()

// Not so smart params order here…
function _isCepageInRecipe( bottle, cepage ) { … }
function _areBuildingsPartOfRecipe ( bottle, buildings ) { … }
// Not so smart params order here…
function _isCepageInRecipe( bottle, cepage ) { … }
function _areBuildingsPartOfRecipe ( bottle, buildings ) { … }
function hasMatchingBottles( cepage, buildings ) {
// Use `_` as a placeholder for not-yet-defined params!
var isCepageInRecipe = _.partial( _isCepageInRecipe, _, cepage );
var areBuildingsPartOfRecipe = _.partial( _areBuildingsPartOfRecipe, _, buildings );
return _( this.get( "bottles" ) )
.filter( isCepageInRecipe )
.any( areBuildingsPartOfRecipe );
}
// Not so smart params order here…
function _isCepageInRecipe( bottle, cepage ) { … }
function _areBuildingsPartOfRecipe ( bottle, buildings ) { … }
function hasMatchingBottles( cepage, buildings ) {
// Use `_` as a placeholder for not-yet-defined params!
var isCepageInRecipe = _.partialRight( _isCepageInRecipe, cepage );
var areBuildingsPartOfRecipe = _.partialRight( _areBuildingsPartOfRecipe, buildings );
return _( this.get( "bottles" ) )
.filter( isCepageInRecipe )
.any( areBuildingsPartOfRecipe );
}
PARTIAL APPLICATION: THE SWISS ARMY KNIFEPARTIAL APPLICATION: THE SWISS ARMY KNIFE

COMPOSITION COMPOSITION VS.VS. CHAINING ? CHAINING ?
_.flow is a tool to build higher order functions
It can eventually replace simple chaining…
… but is not meant to replace _.chain
function getJuices( items ) {
return _( items )
.compact()
.reject( { quantity: 0 } )
.filter( { type: "juice" } )
.value();
}
// Flow equivalent
var getJuices = _.flow(
_.partialRight( _.filter, { type: "juice" } ),
_.partialRight( _.reject, { quantity: 0 } ),
_.compact
);

BUT WAIT, THERE'S BUT WAIT, THERE'S MOREMORE
> FEW > FEW SUBTLETIESSUBTLETIES AND OTHER CLASSICS AND OTHER CLASSICS

NOTNOT EVERYTHING IS CHAINABLE EVERYTHING IS CHAINABLE
Some methods are: _.keys, _.map, _.push, _.pluck,
_.union, …

Others are not — by default: _.find, _.isNumber,
_.reduce, _.sum, …

NON-CHAINABLES METHODSNON-CHAINABLES METHODS
function getJuiceTotalQuantity () {
return _( this.get( "items" ) )
.compact()
.filter( isJuice )
.pluck( "quantity" )
.sum();
// => return the sum
// no need for `.value()` -> implicitly called
}
More in the doc > https://lodash.com/docs#_

_.PROTOTYPE.PLANT(_.PROTOTYPE.PLANT( VALUE VALUE ))
Create a clone of the chain with the given value
var wrapper = _( [ 1, 2, null, 3 ] ).compact();
var otherWrapper = wrapper.plant( [ "a", null, "b", undefined ] );
wrapper.value();
// => [ 1, 2, 3 ]
otherWrapper.value();
// => [ "a", "b" ]

_.PROTOTYPE.COMMIT()_.PROTOTYPE.COMMIT()
Execute the chain and return a wrapper.
var array = [ 1, 2, 3 ];
var wrapper = _( array ).push( 2 );
console.log( array );
// => [ 1, 2, 3 ]
// Nothing executed, nothing changed.
wrapper = wrapper.commit();
console.log( array );
// => [ 1, 2, 3, 2 ]
// Chain executed
// `_.push()` mutated the original `array`
wrapper.without( 2 ).value();
// => [ 1, 3 ]

_.TAP(_.TAP( VALUE, INTERCEPTOR, [THISARG] VALUE, INTERCEPTOR, [THISARG] ))
Invoke interceptor and return value.
_( [ 1, 2, null, 3 ] )
.compact()
.tap( function ( value ) {
console.log( "tapped ->", value );
} )
.push( 1 )
.value();
// => "tapped -> [ 1, 2, 3 ]"
// => "[ 1, 2, 3, 1 ]"
"Tap" into the chain = very useful for debug!

_.TAP(_.TAP( VALUE, INTERCEPTOR, [THISARG] VALUE, INTERCEPTOR, [THISARG] ))
To log an intermediate value
_( [ 1, 2, null, 3 ] )
.compact()
// Can use `console.log`, just don't forget to bind the context!
.tap( console.log.bind( console ) )
.push( 1 )
.value();
// => "[ 1, 2, 3 ]"
// => "[ 1, 2, 3, 1 ]"

_.THRU(_.THRU( VALUE, INTERCEPTOR, [THISARG] VALUE, INTERCEPTOR, [THISARG] ))
Invoke interceptor and return the value of interceptor.
_( [ 1, 2, null, 3 ] )
.compact()
.thru( function ( value ) {
console.log( "tapped ->", value );
// Don't forget to return a value for the chain.
return value.reverse();
} )
.push( 0 )
.value();
// => "tapped -> [ 1, 2, 3 ]"
// => "[ 3, 2, 1, 0 ]"

THANKS!THANKS! ANY QUESTION? ANY QUESTION?