Remove the Boilerplate

Gleb Bahmutov

KENSHO

#boscc

> 200 public NPM modules

> 300 GitHub repos

Node / Angular / QUnit / Mocha / ...

> 300 blog posts

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
  • tests helpers

Tools for shorter code

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

name and argument order

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);
});
const base10 = _.partialRight(parseInt, 10)
['1', '2', '3'].map(base10); 
// [1, 2, 3]
// radix is bound, 
// index and array arguments are ignored

Kensho       Lodash and Ramda

Partial application with placeholders

// function parseInt(x, radix)
const base10 = _.partialRight(parseInt, 10)
['1', '2', '3'].map(base10);
// [1, 2, 3]
// or
var S = require('spots');
const base10 = S(parseInt, S, 10)
['1', '2', '3'].map(base10); 
// [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

Kensho       Node

Example:

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

still boilerplate

Make unary functions

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

Kensho       Node

byK and print expect 1 argument

Then 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

Data is passed implicitly

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

JS functions

function add (a, b) { }
var result = add (...)

Kensho       Unary functions

add
fn1
fn2

always 1 output

Unary functions

function add (a, b) { }
var add10 = add.bind(null, 10)
var result = add10 (3)

Kensho       Unary functions

add10
fn1
fn2

1 output

1 input

JS functions

Kensho       Unary functions

Unary functions

Kensho       Unary functions

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       Curry

var mul = _.curry(function (a, b, c) {
  return a * b * c;
});
mul(2)(3)(10) // 60

Curried functions make any function unary

Kensho       Curry

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 all numbers by 5

var R = require('ramda')
R.map(R.multiply(5), array)

Kensho       API design

// same as
R.map(R.multiply(5))(array)

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 multiplyAll = R.map(R.multiply(constant));
var print = R.bind(console.log, console);

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 is like R.compose

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

No more boilerplate in our synchronous code

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

Kensho       defensive code

Small performance penalty is worth it for shorter unit tests

Tests taking too long? Your project is too large.

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

#boscc

1PM in room "Deborah Sampson" -  Securing the front end, from a Node server

Remove the boilerplate - Boston Code Camp 25

By Gleb Bahmutov

Remove the boilerplate - Boston Code Camp 25

How to remove unnecessary code from your code, including asynchronous code and unit tests. Presented at Boston Code Camp 25 on April 2nd 2016

  • 1,221
Loading comments...

More from Gleb Bahmutov