Remove the Boilerplate

Gleb Bahmutov

KENSHO

var add = function (a, b) {
  return a + b;
};

4 entities: a, b, add and a function

Each entity can interact with other 3

Kensho       Node

var add = function (a, b) {
  return a + b;
};

for N entities: N * (N - 1) interactions

Kensho       Node

Errors = more * code^2
Errors=morecode2Errors = more * code^2
E = m * c^2
E=mc2E = m * c^2

Kensho       Node

Goal

Replace the boilerplate with equivalent short and readable code

Kensho       Node

Example

var path = require('path');
var first = path.join(__dirname, '../foo');
...
var second = path.join(__dirname, '../bar');

Kensho       Node

Example

var join = require('path').join;
var first = join(__dirname, '../foo');
...
var second = join(__dirname, '../bar');

Kensho       Node

Example

var relativePath = require('path')
    .join.bind(null, __dirname);
var first = relativePath('../foo');
...
var second = relativePath('../bar');

Kensho       Node

Example

var first = path.join(__dirname, '../foo');
// vs
var second = relativePath('../bar');

Kensho       Node

  • functional programming 
  • promises
  • helpers for unit tests

Partial application

var relativePath = path.join.bind(null, __dirname);
relativePath('foo.js');

Functional programming

Kensho       Node

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB);
newFn(valueC);

Partial application

Kensho       Node

Place on the left arguments that are likely to be known first 

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB);
newFn(valueC);

When designing signatures

Kensho       Node

function updateUserInfo(userId, newInfo){ ... }
// vs
function updateUserInfo(newInfo, userId){ ... }

Example

Kensho       Node

// user-service.js
function updateUserInfo(userId, newInfo) { ... }
// user-controller.js
function onLogin(id) {
    var updateUser = updateUserInfo.bind(null, id);
    $('form').on('submit', function onSubmitted(form) {
        updateUser(form);
    });
}

Example

Kensho       Node

// user-service.js
function updateUserInfo(userId, newInfo) { ... }
// user-controller.js
function onLogin(id) {
    var updateUser = updateUserInfo.bind(null, id);
    $('form').on('submit', function onSubmitted(form) {
        updateUser(form);
    });
}

Functional programming

Point-free programming

Kensho       Node

// user-service.js
function updateUserInfo(userId, newInfo) { ... }
// user-controller.js
function onLogin(id) {
    var updateUser = updateUserInfo.bind(null, id);
    $('form').on('submit', updateUser);
}

Point-free programming

Functional programming

Kensho       Node

I use flexible partial application to achieve point-free programming style

Kensho       Node

Partial application from the left

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB);

Kensho       Node

// or
var _ = require('lodash');
var newFn = _.partial(fn, valueA, valueB);
// or
var R = require('ramda');
var newFn = R.partial(fn, valueA, valueB);

Partial application from the right

['1', '2', '3'].map(parseInt); // [1, NaN, NaN]
// function parseInt(x, radix)
// but Array.map sends (value, index, array)
['1', '2', '3'].map(function (x) {
    return parseInt(x, 10);
});
['1', '2', '3'].map(_.partialRight(parseInt, 10)); 
// [1, 2, 3]
// radix is bound, 
// index and array arguments are ignored

Kensho       Node

Partial application with placeholders

// function parseInt(x, radix)
['1', '2', '3'].map(_.partial(parseInt, _, 10));
// [1, 2, 3]
// or
var S = require('spots');
['1', '2', '3'].map(S(parseInt, S, 10)); 
// [1, 2, 3]

Kensho       Node

Example: routing placeholders

// Express
// same callbacks to check if the user is logged in and authorized
app.get('/repos', 
    passport.isAuthenticated, passport.isAuthorized, ctrl.getRepos);
app.get('/repos/:user/:name', 
    passport.isAuthenticated, passport.isAuthorized, ctrl.getRepo);
app.get('/repos/view/:user/:name', 
    passport.isAuthenticated, passport.isAuthorized, ctrl.viewFile);
app.get(<url>, passport.isAuthenticated, passport.isAuthorized, ...);

Kensho       Node

Example: routing placeholders

// prefill 2 middle arguments using spots
var S = require('spots');
var authGet = S(app.get, S, 
    passport.isAuthenticated, passport.isAuthorized)
    .bind(app);
// authGet(<url>, <controller>)

Kensho       Node

authGet('/repos', ctrl.getRepos);
authGet('/repos/:user/:name', ctrl.getRepo);
authGet('/repos/view/:user/:name', ctrl.viewFile);

Partial application by name

// divide by 10
function divide(a, b) { return a / b; }
var selective = require('heroin');
var by10 = selective(divide, { b: 10 });
console.log(by10(10)); // 1 (a = 10, b = 10)
console.log(by10(2)); // 0.2 (a = 2, b = 10)

Kensho       Node

Partial application by key

function fn(options) { ... }
var obind = require('obind');
var withBar = obind(fn, { bar: 'bar' });
withBar({ baz: 'baz' });
/*
equivalent to
foo({
    bar: 'bar',
    baz: 'baz'
})
*/

Kensho       Node

Working with arrays without boilerplate

var numbers = [3, 1, 7];
var constant = 2;
var k = 0;
for(k = 0; k < numbers.length; k += 1) {
  console.log(numbers[k] * constant);
}
// 6 2 14

Kensho       Node

Use ES5 methods

function mul(a, b) {
  return a * b;
}
function print(n) {
  console.log(n);
}
numbers.map(function (n) {
  return mul(n, constant);
}).forEach(print);
// 6 2 14

Kensho       Node

Use ES5 methods

function mul(a, b) {
  return a * b;
}
var byK = mul.bind(null, constant);
var print = console.log.bind(console);
numbers
  .map(byK)
  .forEach(print);

Kensho       Node

Use Lodash

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
var _ = require('lodash');
var byK = _.partial(mul, constant);
var print = _.bind(console.log, console);
_.forEach(_.map(numbers, byK), print);

Kensho       Node

var mul = _.curry(function (a, b) {
  return a * b;
});
var byK = mul(constant);
function mul(a, b) {
  return a * b;
}
var byK = _.partial(mul, constant);

Curried functions have partial application built in.

Kensho       Node

Order of arguments

function cb(item, index, array) { ... }
// ES5 method
Array.prototype.map(cb);
// Lodash method
_.map(array, cb);

Place on the left arguments that are likely to be known first 

Kensho       Node

Ramda library

var R = require('ramda');
// callback is first argument
R.map(cb, array);
// all functions are curried
var by5 = R.multiply(5);
by5(10); // 50

Kensho       Node

Multiply then print

var numbers = [3, 1, 7];
var constant = 2;
var R = require('ramda');
var print = R.bind(console.log, console);
var multiplyAll = R.map(R.multiply(constant));
var printAll = R.forEach(print);
printAll(multiplyAll(numbers));

Kensho       Node

Composition

var multiplyAll = R.map(R.multiply(constant));
var printAll = R.forEach(print);
printAll(multiplyAll(numbers));

Kensho       Node

Compose

var multiplyAll = R.map(R.multiply(constant));
var printAll = R.forEach(print);
var computation = R.compose(printAll, multiplyAll);
computation (numbers);

Static logic

Dynamic data

Kensho       Node

R.pipe and R.tap

var mulPrint = R.pipe(
  R.map(R.multiply(constant)),
  R.tap(debugLog),
  R.forEach(R.bind(console.log, console))
);
mulPrint (numbers);

R.pipe for readability

R.tap for debugging

Kensho       Node

Asynchronous code

var glob = require('glob');
function getJavaScriptFiles(cb) {
  glob('*.js', function (err, files) {
    if (err) {
      return cb(err);
    }
    cb(null, files);
  });
}
getJavaScriptFiles(function (err, files) {
  if (err) {
    return console.error(err);
  }
  console.log(files);
});

Kensho       Node

Promises

var getJavaScriptFiles = require('q')
    .denodeify(require('glob'))
    .bind(null, '*.js');
getJavaScriptFiles()
    .then(console.log)
    .catch(console.error)
    .done();

Q

Kensho       Node

Promises with boilerplate

var Q = require('q');
function asyncF() {
  var defer = Q.defer();
  process.nextTick(function () {
    defer.resolve('f');
  });
  return defer.promise;
}
function asyncAddOo(value) {
  var defer = Q.defer();
  process.nextTick(function () {
    defer.resolve(value + 'oo');
  });
  return defer.promise;
}
function print(x) {
  console.log('value =', x);
  return x;
}
function verify(x) {
  console.assert(x === 'foo');
}
asyncF()
  .then(asyncAddOo)
  .then(print)
  .then(verify)
  .done();
// "foo"

Start without boilerplate

var Q = require('q');
function asyncAddOo(value) {
  var defer = Q.defer();
  process.nextTick(function () {
    defer.resolve(value + 'oo');
  });
  return defer.promise;
}
function print(x) {
  console.log('value =', x);
  return x;
}
function verify(x) {
  console.assert(x === 'foo');
}
Q('f')
  .then(asyncAddOo)
  .then(print)
  .then(verify)
  .done();

Kensho       Node

Step without boilerplate

var Q = require('q');
function addOo(value) {
  return value + 'oo';
}
function print(x) {
  console.log('value =', x);
  return x;
}
function verify(x) {
  console.assert(x === 'foo');
}
Q('f')
  .then(addOo)
  .then(print)
  .then(verify)
  .done();

Kensho       Node

Step without boilerplate (2)

var Q = require('q');
var R = require('ramda');
function print(x) {
  console.log('value =', x);
  return x;
}
function verify(x) {
  console.assert(x === 'foo');
}
Q('f')
  // apply second arg
  .then(R.add(R.__, 'oo'))
  .then(print)
  .then(verify)
  .done();

Kensho       Node

Use tap instead of return

var Q = require('q');
var R = require('ramda');
function print(x) {
  console.log('value =', x);
}
function verify(x) {
  console.assert(x === 'foo');
}
Q('f')
  // apply second arg
  .then(R.add(R.__, 'oo'))
  .then(R.tap(print))
  .then(verify)
  .done();

Kensho       Node

Promise pipes

var Q = require('q');
var R = require('ramda');
function print(x) {
  console.log('value =', x);
}
function verify(x) {
  console.assert(x === 'foo');
}
var printThenVerify = R.pipeP(
  R.add(R.__, 'oo'),
  R.tap(print),
  verify
);
Q('f')
  .then(printThenVerify);

Remove boilerplate while the code is still readable to you

Kensho       Node

Use promise library API

fs.readdirAsync('.')
    .map(processFile, { concurrency: 3 })
    .then(fileResults);

Kensho       Node

Remove boilerplate from unit tests

Use a better framework

QUnit.test('a test', function(assert) {
  var done = assert.async();
  asyncOperation()
    .then(function () {
      // assert something
      done();
    });
});

Kensho       Node

Use a better framework

// Mocha
it('works', function (done) {
  asyncOperation()
    .then(function () {
      // assert something
      done();
    });
});

Kensho       Node

Use a better framework

// Mocha
it('works', function () {
  return asyncOperation()
    .then(function () {
      // assert something
    });
});

Kensho       Node

Write a test wrapper

// typical AngularJS unit test
describe('typical test', function () {
    var $rootScope, foo;
    beforeEach(function () {
        angular.mock.module('A');
        // other modules
    });
    beforeEach(inject(function (_$rootScope_, _foo_) {
        $rootScope = _$rootScope_;
        foo = _foo_;
    }));
    it('finally a test', function () {
        $rootScope.$apply(); // for example
        expect(foo).toEqual('bar');
    });
});

Kensho       Node

Write a test wrapper

ngDescribe({
    modules: 'A',
    inject: ['$rootScope', 'foo'],
    tests: function (deps) {
        it('finally a test', function () {
            deps.$rootScope.$apply();
            expect(deps.foo).toEqual('bar');
        });
    });
});

Kensho       Node

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar');
});

    test "does something" failed
    Error:

What has actually failed and why?

Kensho       Node

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo to equal "bar"');
});

    test "does something" failed
    Error: expected foo to equal "bar"

Why did it fail?

Kensho       Node

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo ' + foo + ' to equal "bar"');
});

    test "does something" failed
    Error: expected foo something to equal "bar"

Message repeats the predicate!

Kensho       Node

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo ' + JSON.stringify(foo) + 
    ' to equal "bar"');
});

Kensho       Node

Lazy assertions

// require('lazy-ass');
it('does something', function () {
  la(foo === 'bar', 
    'expected foo', foo, 'to equal "bar"');
});

Kensho       Node

Test helper

var helpfulDescribe = require('lazy-ass-helpful');
helpfulDescribe(function tests() {
  it('does something', function () {
    la(foo === 'bar');
  });
});

Kensho       Node

Test helper

var helpfulDescribe = require('lazy-ass-helpful');
helpfulDescribe(function tests() {
  it('does something', function () {
    la(foo === 'bar', 
      'condition [foo === "bar"], foo is', foo);
  });
});

Executed code

Kensho       Node

Boilerplate and ES6

// need all arguments after "y"
function f(x, y) {
  var a = Array.prototype.slice.call(arguments, 2);
  ...
};
// using lodash
function f(x, y) {
  // need all arguments after "y"
  var a = _.toArray(arguments).slice(2);
  ...
};
// using ES6 rest parameter
function f(x, y, ...a) {
  // a is an array of arguments after "y"
  ...
};

Kensho       Node

ES6 default values

function f (x, y, z) {
    if (y === undefined)
        y = 7;
    if (z === undefined)
        z = 42;
    return x + y + z;
};
// es6
function f (x, y = 7, z = 42) {
    return x + y + z
}

Kensho       Node

ES6 computed keys

var foo = '...';
var o = {};
o[foo] = 42;
// es6
var foo = '...';
var o = {
  [foo]: 42
}

Kensho       Node

ES6 arrow functions

numbers.map(function (v) { return v + 1; });
// es6
numbers.map(v => v + 1);
// I prefer
var R = require('ramda');
var add1 = R.add(1);
numbers.map(add1);

Kensho       Node

Conclusions

  • Small reusable functions
  • Design signatures for partial application
  • Combine functions into pipelines
  • Use promises
  • Deal with boilerplate in your unit tests

Kensho       Node

Remove the Boilerplate