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

Remove the boilerplate

By Gleb Bahmutov

Remove the boilerplate

How to remove unnecessary code from your code, including asynchronous code and unit tests. Presented at NodeJS Italy in 2015

  • 7,971