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 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( 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

COmposition et autres ruses

> Pour construire des pipelines efficaces

Composition ?

(f\cdot g)(x) = f(g(x))
(fg)(x)=f(g(x))(f\cdot 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 ]"

Merci ! Des questions ?

Chaining et composition de fonctions avec lodash / underscore

By Nicolas Carlo

Chaining et composition de fonctions avec lodash / underscore

Chaining et composition de fonctions avec lodash / underscore. Talk présenté le 23 juin 2015 au meetup Backbone.js Paris S01E07.

  • 2,315