Remove the Boilerplate

Gleb Bahmutov

KENSHO

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vulputate, orci non porta dignissim, ipsum neque hendrerit neque, in molestie metus dolor quis erat. Sed semper sit function orci sed sagittis. Nulla ut ultricies arcu, eu rutrum nisi. In hac habitasse platea dictumst. Ut vel add (tellus. Ut a, suscipit b) diam interdum tortor tincidunt, { eu consectetur leo efficitur. Proin return a + b; tortor nisi, dignissim eu porta non, auctor in nisi. Cras suscipit augue nec felis ornare, non } elementum dolor blandit. Vestibulum auctor commodo nunc, id interdum ex auctor eu. Vivamus gravida interdum viverra. Nunc porta sodales ipsum, a tempus leo placerat vel. Quisque ut massa id elit facilisis porttitor at efficitur velit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vulputate, orci non porta dignissim, ipsum neque hendrerit neque, in molestie metus dolor quis erat. Sed semper sit function orci sed sagittis. Nulla ut ultricies arcu, eu rutrum nisi. In hac habitasse platea dictumst. Ut vel add (tellus. Ut a, suscipit b) diam interdum tortor tincidunt, { eu consectetur leo efficitur. Proin return a + b; tortor nisi, dignissim eu porta non, auctor in nisi. Cras suscipit augue nec felis ornare, non } elementum dolor blandit. Vestibulum auctor commodo nunc, id interdum ex auctor eu. Vivamus gravida interdum viverra. Nunc porta sodales ipsum, a tempus leo placerat vel. Quisque ut massa id elit facilisis porttitor at efficitur velit.
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

Human brain has limit: 3 - 7 things at a time

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

Kensho       Node

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

Removed unnecessary variable add

Kensho       Node

Goal: replace the boilerplate with equivalent short and readable code

Kensho       Node

Tools: ES5, FP, ES6

Kensho       Node

Balance: readability, testability, performance 

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       JavaScript

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

Partial application

Kensho       JavaScript

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);

Design the signatures

Kensho       JavaScript

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

Example

Kensho       JavaScript

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

Example

Kensho       JavaScript

$('form').on('submit', function onSubmitted(form) {
    updateUser(form);
});

Partial application from the left

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

Kensho       Lodash and Ramda

// 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       Lodash and Ramda

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       Lodash and Ramda

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       Lodash and Ramda

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>)
authGet('/repos', ctrl.getRepos);
authGet('/repos/:user/:name', ctrl.getRepo);
authGet('/repos/view/:user/:name', ctrl.viewFile);

Kensho       Lodash and Ramda

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       Lodash and Ramda

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       Lodash and Ramda

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

pointfree style

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

pointfree style

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       API design

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       API design

Multiply then print

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

Kensho       API design

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));

Pointfree allows composition

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

Kensho       FP

f(g(x)) = (f \circ g)(x)
f(g(x))=(fg)(x)f(g(x)) = (f \circ g)(x)

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       FP

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       FP

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       Promises

Promises

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

Q

Kensho       Promises

No more error handling or callback boilerplate

Start without boilerplate

var Q = require('q');
function asyncF() {
  var defer = Q.defer();
  process.nextTick(function () {
    defer.resolve('f');
  });
  return defer.promise;
}
Q('f')
// or
Promise.resolve('f')

Kensho       Promises

.then without boilerplate

promise
    .then(function (x) {
        return new Promise(function (resolve) {
            resolve(x + 1)
        })
    })

Kensho       Promises

promise
    .then(function (x) {
        return x + 1
    })
// or
promise.then(R.inc)

Use "tap" to inspect value

Kensho       Promises

promise
    ...
    .then(function (x) {
        console.log(x)
        return x
    })
promise
    ...
    .then(R.tap(console.log))

Promise composition

Q(...)
    .then(R.inc)
    .then(R.tap(console.log))
    .then(verify)
    .done()
var printThenVerify = R.pipeP(
  R.inc,
  R.tap(console.log),
  verify
)
Q(...)
  .then(printThenVerify)
  .done()

Kensho       Promises

Use promise library API

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

Kensho       Promises

Advanced: generators, async / await, reactive streams

Kensho       RxJS

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       testing

Use a better framework

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

Kensho       testing

Use a better framework

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

Kensho       testing

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       OSS

Write a test wrapper

ngDescribe({
    modules: 'A',
    inject: 'foo',
    tests: function (deps) {
        it('finally a test', function () {
            deps.step()
            expect(deps.foo).toEqual('bar')
        })
    })
})

Kensho       OSS

Boilerplate in assertions

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

    test "does something" failed
    Error:

What has actually failed and why?

Kensho       defensive code

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       defensive code

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       defensive code

Boilerplate in assertions

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

Kensho       defensive code

Lazy assertions

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

Kensho       defensive code

Test helper

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

Kensho       defensive code

JS engine executes

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

Kensho       defensive code

lazy-ass-helpful rewrites code using falafel

Boilerplate and ES6

// ES5: 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);
  ...
};

Kensho       ES6

Boilerplate and ES6

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

Kensho       ES6

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       ES6

ES6 computed keys

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

Kensho       ES6

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       ES6

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

KENSHO

Remove the boilerplate - NationJS

By Gleb Bahmutov

Remove the boilerplate - NationJS

How to remove unnecessary code from your code, including asynchronous code and unit tests. Presented at NationJS Node day 2016 in Washington DC.

  • 6,615