Skip to content

Latest commit

 

History

History
1054 lines (630 loc) · 19 KB

javascript-combinators-2017.md

File metadata and controls

1054 lines (630 loc) · 19 KB

footer: © 2015-2017 Reginald Braithwaite. Some rights reserved. slidenumbers: true

original

^ https://www.flickr.com/photos/faceme/7759225036


JavaScript Combinators

"306th prime edition"

^ https://www.flickr.com/photos/faceme/7759225036


original

^ https://www.flickr.com/photos/mariannebevis/8636118515


we'll talk about

Using combinators for decomposition and composition

^ https://www.flickr.com/photos/mariannebevis/8636118515


original

^ http://www.wikiart.org/en/piet-mondrian/avond-evening-the-red-tree-1910

^ "Making responsibilities and relationships explicit"


and we'll think about

Making responsibilities and relationships explicit

^ http://www.wikiart.org/en/piet-mondrian/avond-evening-the-red-tree-1910


original

^ https://www.flickr.com/photos/npobre/2601582256


original

^ https://www.flickr.com/photos/westher/7709055816


Decomposition

^ https://www.flickr.com/photos/westher/7709055816


We decompose entities to make discreet responsibilities explicit


a monolith

Parse.User.logIn("user", "pass", {
  success: function (user) {
    query.find({
      success: function (users) {
        users[0].save({ key: value }, {
          success: function (user) {
            currentUser = user;
          }
        });
      }
    });
  }
});

^ Adapted from http://blog.parse.com/learn/engineering/whats-so-great-about-javascript-promises/


decomposition by extracting functions

let assignCurrentUser = (user) => { currentUser = user; };

let saveFirstUser = (users) =>
  users[0].save({ key: value }, {
    success: assignCurrentUser
  });

let logUserIn = (user) =>
  query.find({
    success: saveFirstUser
  });

Parse.User.logIn("user", "pass", { success: logUserIn });

^ Adapted from http://blog.parse.com/learn/engineering/whats-so-great-about-javascript-promises/


original

^ https://www.flickr.com/photos/mariannebevis/8646014265


Composition

^ https://www.flickr.com/photos/mariannebevis/8646014265


We compose entities to make the relationships between them explicit


promises explicitly compose asynchronous functions

let findUser = (user) => query.find();

let saveFirstUser = (user) => users[0].save({ key: value });

let assignCurrentUser = (user) => { currentUser = user; };

Parse.User.logIn("user", "pass")
  .then(findUser)
  .then(saveFirstUser)
  .then(assignCurrentUser);

^ Adapted from http://blog.parse.com/learn/engineering/whats-so-great-about-javascript-promises/


original

^ https://www.flickr.com/photos/jforth/4467184640


Decomposition is about entities

^ https://www.flickr.com/photos/jforth/4467184640


Composition is about relationships

^ https://www.flickr.com/photos/jforth/4467184640


Back to decomposition


extracting named functions

The most obvious form of decomposition


original

^ https://www.flickr.com/photos/guysie/3325635275


Extracting functions works from the inside-out

^ https://www.flickr.com/photos/guysie/3325635275


extracting named functions

Decomposition of Implementation


Let's look at something else


original

^ https://www.flickr.com/photos/130973380@N08/17185509447

^ "pluck"


pluck: "A convenient version of what is perhaps the most common use-case for map, extracting a list of property values."

^ https://www.flickr.com/photos/130973380@N08/17185509447

^ underscorejs.org


let pluck = (collection, property) =>
  collection.map( (obj) => obj[property] );

var deStijl = [
  {name: 'Theo van Doesburg', occupation: 'theorist'},
  {name: 'Piet Mondriaan', occupation: 'painter'},
  {name: 'Gerrit Rietveld', occupation: 'architect'}];

pluck(deStijl, 'name')
  //=> ["Theo van Doesburg", "Piet Mondriaan", "Gerrit Rietveld"]

functions have interfaces

pluck's interface has two parts: The collection, and the property


manually decomposing pluck's interface

var deStijl = [
  {name: 'Theo van Doesburg', occupation: 'theorist'},
  {name: 'Piet Mondriaan', occupation: 'painter'},
  {name: 'Gerrit Rietveld', occupation: 'architect'}];

let pluckFrom = (collection) =>
  (property) => pluck(collection, property);

pluckFrom(deStijl)('name')
  //=> ["Theo van Doesburg", "Piet Mondriaan", "Gerrit Rietveld"]

manually decomposing pluck's interface

let pluckWith = (property) =>
  (collection) => pluck(collection, property);

pluckWith('name')(deStijl)
  //=> ["Theo van Doesburg", "Piet Mondriaan", "Gerrit Rietveld"]

pluckFrom and pluckWith partially apply pluck


origina

^ https://www.flickr.com/photos/16210667@N02/18067981830


Partial application decomposes functions from the outside-in

^ https://www.flickr.com/photos/16210667@N02/18067981830


partial application

Decomposition of Interface


original

^ https://www.flickr.com/photos/vauvau/23067022785


Decorators

^ https://www.flickr.com/photos/vauvau/23067022785


A decorator is a higher-order function that takes a function, and returns another function that adds to or modifies its argument's behaviour.


partial application decomposes a function from the outside-in

let pluck = (collection, property) =>
  collection.map( (obj) => obj[property] );

// decomposes into:

let pluckFrom = (collection) =>
  (property) => pluck(collection, property);

^ http://raganwald.com/2013/03/07/currying-and-partial-application.html


extract closed-over binding

let pluckFrom = (collection) =>
  (property) => pluck(collection, property);

// extract `pluck`:

let ____ = (pluck, collection) =>
  (property) => pluck(collection, property);

// rename:

let leftApply = (fn, a) =>
  (b) => fn(a, b);

using leftApply

let pluckFrom = (collection) =>
  leftApply(pluck, collection);

// again:

let pluckFrom = leftApply(leftApply, pluck);

// again

let pluckFrom = leftApply(leftApply, leftApply)(pluck);

^ https://www.flickr.com/photos/saramarlowe/8170948596


hmmmm

What is leftApply(leftApply, leftApply)?

^ https://www.flickr.com/photos/saramarlowe/8170948596


back to partial application

let rightApply = (fn, b) =>
  (a) => fn(a, b);

let pluckWith = (property) => (pluck, property);

// again:

let pluckWith = leftApply(rightApply, pluck);

// again:

let pluckWith = leftApply(leftApply, rightApply)(pluck);

combinators that decompose functions

// leftApply(leftApply, leftApply)

let Istarstar = (a) => (b) => (c) => a(b, c);

let pluckFrom = Istarstar(pluck);

// leftApply(leftApply, rightApply)

let C = (a) => (b) => (c) => a(c, b)

let pluckWith = C(pluck);

^ "cardinal"


more decomposition with combinators

let get = (object, property) =>
  object[property];

get({name: 'Gerrit Rietveld'}, 'name')
  //=> Gerrit Rietveld

let getWith = C(get);

let nameOf = getWith('name');

nameOf({name: 'Gerrit Rietveld'})
  //=> Gerrit Rietveld

original

^ http://www.panoramio.com/photo/82281274


get is a function taking two arguments

getWith is a decomposition of get that names one part

nameOf is a decomposition of get that names and specifies one part

^ http://www.panoramio.com/photo/82281274


last one

let map = (collection, fn) =>
  collection.map(fn);

let mapWith = C(map);

let namesOf = mapWith(nameOf);

original

^ https://www.flickr.com/photos/dalbera/20059027984


map is a higher-order function

mapWith is a decomposition of map that names one part

namesOf is a decomposition of map that names and specifies one part

^ https://www.flickr.com/photos/dalbera/20059027984


Back to composition

^ before we go, now you know what to say when somebody asks "What good is currying?" or "What is the practical purpose of partial application?" It's to decompose functions by responsibility.


^ https://www.flickr.com/photos/ctaweb/8487304182

^ "Simple Composition"


Simple Composition

^ https://www.flickr.com/photos/ctaweb/8487304182


let compose = (a, b) => (c) => a(b(c));

^ 'compose'. B = (a) => (b) => (c) => a(b(c))


compose in action

var deStijl = [
  {name: 'Theo van Doesburg', occupation: 'theorist'},
  {name: 'Piet Mondriaan', occupation: 'painter'},
  {name: 'Gerrit Rietveld', occupation: 'architect'}];

let pluckWith = compose(mapWith, getWith);

let namesOf = pluckWith('name');

namesOf(deStijl)
  //=> ["Theo van Doesburg","Piet Mondriaan","Gerrit Rietveld"]

pluckWith = compose(mapWith, getWith);

^ compose wires the output of one function into the input of another


original

^ https://www.flickr.com/photos/72283508@N00/2444938101

^ "We compose entities to make the relationships between them explicit"

^ and now we know how to answer that question, too!


reminder:

We compose entities to make the relationships between them explicit

^ https://www.flickr.com/photos/72283508@N00/2444938101


More Composition


let mix = (...ingredients) =>
  console.log('mixing', ...ingredients);

let bake = () => console.log('baking');

let cool = () => console.log('cooling');

let makeBread = (...ingredients) => {
  mix(...ingredients);
  bake();
  cool();
}

composition with before

let before = (fn, decoration) =>
  (...args) => {
    decoration(...args);
    return fn(...args);
  };

let bakeBread = before(bake, mix);

let makeBread = (...ingredients) => {
  bakeBread();
  cool();
}

before makes the time relationship between two functions explicit


composition with after

let after = (fn, decoration) =>
  (...args) => {
    let returnValue = fn(...args);
    decoration(...args);
    return returnValue;
  };

let bakeBread = before(bake, mix);

let makeBread = after(bakeBread, cool);

after also makes the time relationship between two functions explicit


decomposing before

let beforeWith = (decoration) =>
  rightApply(before, decoration);

let mixBefore = beforeWith(mix);

let bakeBread = mixBefore(bake);

decomposing after

let afterWith = (decoration) =>
  rightApply(after, decoration);

let coolAfter = afterWith(after);

let makeBread = coolAfter(bakeBread);

beforeWith and afterWith are combinators that turn functions into decorators that compose behaviour


original

^ "These are not the whole story, by far."


original

^ https://www.flickr.com/photos/lightplay/4727131891


JavaScript invocations are coloured

^ https://www.flickr.com/photos/lightplay/4727131891

^ http://raganwald.com/2015/03/12/symmetry.html


coloured decorators

let before = (fn, decoration) =>
  function (...args) {
    decoration.apply(this, args);
    return fn.apply(this, args);
  };

let after = (fn, decoration) =>
  function (...args) {
    let returnValue = fn.apply(this, args);
    decoration.apply(this, args);
    return returnValue;
  };

Why coloured decorators matter


bread, revisited

class Bread {
  constructor (...ingredients) {
    this.ingredients = ingredients;
  }

  mix () {
    console.log('mixing', ...this.ingredients);
  };

  bake () { console.log('baking'); }

  cool () { console.log('cooling'); }
}

bread, revisited

class Bread {

  // ...

  make () {
    this.mix();
    this.bake();
    this.cool();
  }
}

Classes can be decorated too


decorateMethodWith

const decorateMethodWith = (decorator, ...methodNames) =>
  (clazz) => {
    for (let methodName of methodNames) {
      const method = clazz.prototype[methodName];

      Object.defineProperty(clazz.prototype, methodName, {
        value: decorator(method),
        writable: true
      });
    }
    return clazz;
  };

beforeAll and afterAll

const beforeAll = (decorator, ...methodNames) =>
  decorateMethodWith((method) => before(method, decorator), ...methodNames),

      afterAll = (decorator, ...methodNames) =>
  decorateMethodWith((method) => after(method, decorator), ...methodNames);

better bread

let invoke = (methodName) => function (...args) {
  return this[methodName](...args);
}

let BetterBread =
  beforeAll(invoke('mix'), 'make')(
    afterAll(invoke('cool'), 'make')(
      class {
        // ...

        make () {
          this.bake();
        }
      }
    )
  );

original

^ https://www.flickr.com/photos/68112440@N07/6210847796

^ "Looking Forward"


Looking Forward

^ https://www.flickr.com/photos/68112440@N07/6210847796


ES.who-knows-when


better bread with class decorator sugar

let invoke = (methodName) => function (...args) {
  return this[methodName](...args);
}

@beforeAll(invoke('mix'), 'make')
@afterAll(invoke('cool'), 'make')
class AwesomeBread {

  // ...

  make () {
    this.bake();
  }
}

method decorators

let methodDecorator = (decorator) =>
  function (target, name, descriptor) {
    descriptor.value = decorator(descriptor.value);
  }


let invokeBefore = (methodName) =>
  methodDecorator( (methodBody) =>
    before(methodBody, invoke(methodName))
  );

let invokeAfter = (methodName) =>
  methodDecorator( (methodBody) =>
    after(methodBody, invoke(methodName))
  );

better make

class Bread {

  // ...

  @invokeBefore('mix')
  @invokeAfter('cool')
  make () {
    this.bake();
  }
}

original

^ https://www.flickr.com/photos/hogeslag/3404068564


What have we seen so far?

^ https://www.flickr.com/photos/hogeslag/3404068564


What have we seen so far?

  • Extract function
  • Promise interface
  • Partial application
  • Extract closed-over binding

(more!)


What have we seen so far?

  • Simple composition
  • Composition decorators
  • Class decorators
  • Method decorators

Don't worry about the details!


original

^ http://wallpaperswiki.com/wp-content/uploads/2012/10/Simultaneous-Counter-Composition.-Theo-Van-Doesburg-728x546.jpg

^ "Decorators declutter secondary concerns"


it's all the same idea

Decomposition makes responsibilities explicit

^ http://wallpaperswiki.com/wp-content/uploads/2012/10/Simultaneous-Counter-Composition.-Theo-Van-Doesburg-728x546.jpg


original

^ https://en.wikipedia.org/wiki/Theo_van_Doesburg

^ "Composition makes relationships explicit"


and it's all the same idea

Composition makes relationships explicit

^ https://en.wikipedia.org/wiki/Theo_van_Doesburg


These ideas matter


There are only two hard problems in Computer Science: Cache invalidation, and naming things.

^ Phil Karlton


original

^ https://www.flickr.com/photos/michale/2744016741

^ "Naming entities is hard because you have to figure out which entities need to be named"


Naming entities is hard because you have to figure out which entities need to be named

^ https://www.flickr.com/photos/michale/2744016741


original

^ https://www.flickr.com/photos/stretta/5572576057

^ "Naming relationships is hard because you have to figure out which relationships need to be named"


Naming relationships is hard because you have to figure out which relationships need to be named

^ https://www.flickr.com/photos/stretta/5572576057


original

^ https://www.flickr.com/photos/cristiano_betta/2970086666


Combinators do not make naming easy

^ https://www.flickr.com/photos/cristiano_betta/2970086666


original

^ https://www.flickr.com/photos/joao_trindade/4362414729

^ "Combinators give us a language for naming things"


Combinators give us a language for naming things in code

^ https://www.flickr.com/photos/joao_trindade/4362414729


original

^ https://www.flickr.com/photos/suburbanbloke/723665503

^ "Do not follow in the footsteps of the sages: Seek what they sought"


Do not follow
in the footsteps of the sages.

^ https://www.flickr.com/photos/suburbanbloke/723665503


Seek what they sought.

^ https://www.flickr.com/photos/suburbanbloke/723665503


Reg Braithwaite
PagerDuty, Inc.

right, fit

^ https://leanpub.com/javascriptallongesix