Chaining et Composition de fonctions avec _
Avant de plonger
> petit check-up
Quand je dis _
- Personnellement j'utilise lodash (v3.0+)
- Ça marche aussi avec underscore (pour l'essentiel) \o/
Functional Programming?
- First-class functions
- Higher order functions
- Pure functions
- Recursion
- Immutability
- ≠ imperative programming
- mais !≠ OOP
Chaining de fonctions
> et pourquoi faire ?
Cas concret : opérations multiples (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 }
] );
Cas concret : opérations multiples (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 }
// ]
Cas concret : opérations multiples (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 }
// ]
Cas concret : opérations multiples (3)
function getItems() {
return _.filter(
_.reject(
_.compact( this.get( "items" ) ),
{ quantity: 0 }
),
{ type: "juice" }
);
}
getItems();
// =>
// [
// { id: "ae213aa4", cepage: "merlot", type: "juice", quantity: 3 }
// ]
Cas concret : opérations multiples (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( value )
- Creates a
lodash
object that wrapsvalue
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( 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 }
// ]
CAS CONCRET : OPÉRATIONS MULTIPLES (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 }
// ]
Tout ça pour ça ?
> intérêts du chaining
Pipeline / Flow
function getItems() {
return _( this.get( "items" ) )
.compact()
.reject( isEmpty )
.filter( isJuice )
.map( parseText )
// … we construct the pipeline
// flow is clear, readable!
.value();
}
Ça vous rappelle quelque chose ?
Pipeline / 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.
Lazy Evaluation
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!
}
Lazy Evaluation
Pour en savoir plus >
http://filimanjaro.com/blog/2014/introducing-lazy-evaluation/
COmposition et autres ruses
> Pour construire des pipelines efficaces
Composition ?
(f\cdot g)(x) = f(g(x))
(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 ?
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( [funcs] )
- Crée une fonction qui retourne le résultat des
funcs
où chacune est invoquée avec le résultat de la fonction qui la précède, de la droite vers la gauche (= compose).
_.flow( [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
Si _.flowRight
n'est pas intuitif pour vous.
Application partielle : _.partial()
function greet( greeting, name ) {
return greeting + " " + name;
}
var sayHelloTo = _.partial( greet, "Hello" );
// returns a function with params partially set.
sayHelloTo( "Backbone" );
// → "Hello Backbone"
Application partielle
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 );
}
Pour pouvoir chaîner dans la vraie vie…
_.partialRight()
function greet( greeting, name ) {
return greeting + " " + name;
}
var greetBackbone = _.partialRight( greet, "Backbone" );
// returns a function with params partially set.
greetBackbone( "Hello" );
// → "Hello Backbone"
Application partielle : le couteau suisse
// 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 );
}
Composition Vs. Chaining ?
-
_.flow
est un outil pour créer des higher order functions - Peut éventuellement remplacer des chaînes simples…
- … mais pas toujours adapté pour remplacer
_.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 more
> quelques subtilités et grands classiques
Tout n'est pas chainable
- Il y a des méthodes qui le sont :
_.keys
,_.map
,_.push
,_.pluck
,_.union
, …
- D'autres qui ne le sont pas (par défaut) :
_.find
,_.isNumber
,_.reduce
,_.sum
, …
Méthodes non-chainables
function getJuiceTotalQuantity() {
return _( this.get( "items" ) )
.compact()
.filter( isJuice )
.pluck( "quantity" )
.sum();
// => return the sum
// no need for `.value()` -> implicitly called
}
Plus d'infos sur la doc > https://lodash.com/docs#_
_.prototype.plant( value )
Crée un clone de la chaîne avec la value
donnée
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()
Exécute la chaîne et retourne un 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( value, interceptor, [thisArg] )
Invoque interceptor
et retourne 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" dans la chaîne = très utile pour debug !
_.tap( value, interceptor, [thisArg] )
Pour log une valeur intermédiaire
_( [ 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( value, interceptor, [thisArg] )
Invoque interceptor
et retourne la value
de l'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 ]"