Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
Global temperature anomaly 2022 https://climate.nasa.gov/vital-signs/global-temperature/
source: Improved Minesweeper
source: careers.stackoverflow.com
source: modulecounts.com for more: https://blog.sandworm.dev/state-of-npm-2023-the-overview
Changing how JS developers program will have a huge impact on the software quality in the world
number of engineers
lifetime of the company
Let's use FP to implement Reactive Programming patterns, like Netflix does!
- ME (too cheerfully)
2 major parts
Everyone has their own path to JS
! junior vs senior
In theory
anyone
functional reactive programmer
In practice
anyone
functional reactive programmer
...
...
...
...
...
...
...
Problem: given an array of numbers, multiply each number by a constant and print the result.
var numbers = [3, 1, 7];
var constant = 2;
// 6 2 14
more than 20 implementations at
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
"for" loop is a pain to debug and use
mixing data manipulation with printing
code is hard to reuse
Start splitting code into procedures
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
function processNumber(n) {
print(mul(n, constant));
}
for(k = 0; k < numbers.length; k += 1) {
processNumber(numbers[k]);
}
// 6 2 14
Start splitting code into procedures
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
function processNumber(n) {
print(mul(n, constant));
}
for(k = 0; k < numbers.length; k += 1) {
processNumber(numbers[k]);
}
// 6 2 14
reusable simple functions
Start splitting code into procedures
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
function processNumber(n) {
print(mul(n, constant));
}
for(k = 0; k < numbers.length; k += 1) {
processNumber(numbers[k]);
}
// 6 2 14
reusable simple functions
complex logic
Start splitting code into procedures
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
function processNumber(n) {
print(mul(n, constant));
}
for(k = 0; k < numbers.length; k += 1) {
processNumber(numbers[k]);
}
// 6 2 14
reusable simple functions
complex logic
iteration
Boring code does not surprise you
// using "this" WILL surprise you
numbers.forEach(function (x) {
this.doSomething(x);
// ERROR!
});
var self = this;
numbers.forEach(function (x) {
self.doSomething(x);
});
Boring code does not surprise you
// using "this" in your objects
// WILL surprise someone else
numbers.forEach(console.log);
numbers.forEach(console.log.bind(console));
// works under Node
// in Chrome
// Uncaught TypeError: Illegal invocation
It was finally fixed a few years ago 👍
Boring code does not surprise you
// for loops WILL surprise you
for(k = 0; k < numbers.length; k += 1) {
processNumber(numbers[k]);
}
common problems: off by 1 errors, iterator variable, changing array size
Boring code does not surprise you
// using non-local variables WILL surprise you
constant = 2;
...
numbers.forEach(function process(n) {
return mul(n, constant);
});
common problems: undefined values, or values that change suddenly
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
not really pure, but close enough
var mulBy2 = mul.bind(null, 2);
mulBy2(5); // 10
function mul(a, b) {
return a * b;
}
var mul2by3 = mul.bind(null, 2, 3);
mul2by3(); // 6
var mul2By10 = mulBy2.bind(null, 10);
mul2By10('extra', 'args are', 'ignored'); // 20
function mul(a, b) {
return a * b;
}
var by2 = mul.bind(null, 2);
// same as
var _ = require('lodash');
var by2 = _.partial(mul, 2);
Boring code is nice, but I want encapsulated logic, data access, inheritance!
- a jaded developer
function NumberMultiplier() {}
NumberMultiplier.prototype.setNumbers = function (numbers) {
this.numbers = numbers;
return this;
};
NumberMultiplier.prototype.multiply = function (constant) {
for (var k = 0; k < this.numbers.length; k += 1) {
this.numbers[k] = constant * this.numbers[k];
}
return this;
};
NumberMultiplier.prototype.print = function () {
console.log(this.numbers);
};
Object-Oriented JavaScript
Object-Oriented JavaScript
// using NEW keyword
new NumberMultiplier()
.setNumbers(numbers)
.multiply(constant)
.print();
// [ 6, 2, 14 ]
Object-Oriented JavaScript: 3 parts
function NumberMultiplier() {}
NumberMultiplier.prototype.setNumbers = function() {}
NumberMultiplier.prototype.multiply = function() {}
NumberMultiplier.prototype.print = function() {}
new NumberMultiplier();
constructor function
prototype
"new" keyword
// NumberMultiplier.prototype object
{
setNumbers: function () { ... },
multiply: function () { ... },
print: function () { ... }
}
/\
|| [[ prototype ]]
||
// instance object
{
numbers: [3, 1, 7]
}
OO JavaScript is prototypical
var NumberMultiplier = {
setNumbers: function (numbers) {
this.numbers = numbers; return this;
},
multiply: function (constant) {
for (var k = 0; k < this.numbers.length; k += 1) {
this.numbers[k] = constant * this.numbers[k];
} return this;
},
print: function () { console.log(this.numbers); }
};
Prototype is a plain object!
var numberMultiplier = Object.create(NumberMultiplier);
numberMultiplier
.setNumbers(numbers)
.multiply(constant)
.print();
// [ 6, 2, 14 ]
plain object
class NumberMultiplier {
setNumbers(numbers) {
this.numbers = numbers;
return this;
}
multiply(constant) {
for (var k = 0; k < this.numbers.length; k += 1) {
this.numbers[k] = constant * this.numbers[k];
}
return this;
}
print() {
console.log(this.numbers);
return this;
}
}
new NumberMultiplier()
ES6 classes: just don't
typeof [1, 2] // "object"
Array.isArray([1, 2]) // true
Array.prototype
/*
map
forEach
filter
reduce
...
*/
Arrays: object-oriented + functional
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
var mulBy = mul.bind(null, constant);
numbers
.map(mulBy)
.forEach(print);
// 6 2 14
Arrays: object-oriented + functional
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
var mulBy = mul.bind(null, constant);
numbers
.map(mulBy)
.forEach(print);
// 6 2 14
functional bits
Arrays: object-oriented + functional
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
var mulBy = mul.bind(null, constant);
numbers
.map(mulBy)
.forEach(print);
// 6 2 14
functional bits
Object-oriented methods
// OO (built-in)
Array.prototype.map(cb)
// functional (user space)
Library.map(array, cb)
// plus
Library.groupBy(array, cb);
Library.dropWhile(array, cb);
...
var _ = require('lodash');
var byConstant = _.partial(mul, constant);
// _.forEach(array, cb);
// _.map(array, cb);
Work with arrays using Lodash
_.forEach(
_.map(numbers, byConstant),
print
);
// 6 2 14
_({ John: 3, Jim: 10 })
.map(byConstant)
.forEach(print);
// 6 20
var mulBy = ...
// OO (built-in)
Array.prototype.map(mulBy)
// lodash / underscore
_.map(array, mulBy)
// Ramda
R.map(mulBy, array)
var R = require('ramda');
var byConstant = R.partial(R.mul, constant);
// R.forEach = function (callback, array)
var printEach = R.partial(R.forEach, print);
// R.map: function (callback, array)
var mapByConstant = R.partial(R.map, byConstant);
composition
printEach(mapByConstant(numbers));
// 6 2 14
var R = require('ramda');
var byConstant = R.partial(R.mul, constant);
// R.forEach = function (callback, array)
var printEach = R.partial(R.forEach, print);
var algorithm = R.compose(printEach, mapByConstant);
algorithm(numbers);
// 6 2 14
// R.map: function (callback, array)
var mapByConstant = R.partial(R.map, byConstant);
// fn( data )
var R = require('ramda');
var byConstant = R.partial(R.mul, constant);
// R.forEach = function (callback, array)
var printEach = R.partial(R.forEach, print);
var algorithm = R.compose(printEach, mapByConstant);
algorithm(numbers);
// 6 2 14
// R.map: function (callback, array)
var mapByConstant = R.partial(R.map, byConstant);
"fn(data)" code is simple to reason about and test
// fn( data )
// R.mul = function (a, b) { ... }
var byConstant = R.partial(R.mul, constant);
Partial application vs curry
// in Ramda this is same as simply
var byConstant = R.mul(constant);
// for your functions
var mul = R.curry(function (a, b) {
return a * b;
});
mul(5)(4); // 20
var R = require('ramda');
R.pipe(
R.map(R.multiply(constant)),
R.forEach(print)
)(numbers);
// 6 2 14
Curry: program with very little code
Requires careful function signature design
i.e. put information most likely to be known early as first argument
"Smart" objects
Pass data around
"Dumb" objects
"Smart" pipeline
Pass code (functions) around
new NumberMultiplier()
.add(3, 1, 7)
.multiply(2)
.print();
_.forEach(
_.map(numbers, byConstant),
print
);
It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures."
- Alan J. Perlis (Algol)
new NumberMultiplier()
.add(3, 1, 7)
.multiply(2)
.print();
_.forEach(
_.map(numbers, byConstant),
print
);
Source of complexity: mutable data
var numbers = [...];
numbers.forEach(processNumber);
// is the list "numbers" unchanged?
function processNumber(x, k, array) {
array[1] = 10000;
print(x);
}
Mutable data makes code hard to understand and modify
Immutable data structures
'use strict';
var immutable = require('seamless-immutable');
var byConstant = _.partial(mul, constant);
immutable(numbers)
.map(byConstant)
.forEach(function (x, k, array) {
array[1] = 10000; // throws an Error
print(x);
});
Efficient immutable JS libraries: seamless-immutable, mori
Immutable data with Redux pattern
function mul(a, b) { return a * b; }
function multiply(state, constant) {
var byConstant = mul.bind(null, constant);
return state.updateIn(['numbers'], function (ns) {
return ns.map(byConstant);
});
}
var immutable = require('immutable');
var initialState = immutable.Map({
numbers: immutable.List.of([3, 1, 7])
});
var newState = multiply(initialState, constant);
// newState !== initialState
Efficient computation with transducers
Do as little as needed with lazy evaluation
// Does NOT work
numbers.map(function (x, done) {
mul(x, function cb(result) {
done(result);
})
}).forEach(function (value) {
print(value, function cb() {
done():
})
});
How to handle async callbacks during iteration?
numbers
.map(asyncFn)
.forEach(asyncFn)
const p = new Promise((resolve, reject) => {
...
resolve(42)
})
// after promise successfully completes
p.then(...)
.catch(...)
Single async operation with a value or an error
Each callback can be synchronous or return a Promise
function sleep () {
return new Promise(resolve => {
setTimeout(resolve, 1000)
})
}
p.then(sleep)
var sleepSecond = _.partial(Q.delay, 1000);
var pauseMulAndPrint = function (n) {
return function () {
return sleepSecond()
.then(_.partial(byConstant, n))
.then(print);
};
};
numbers.map(pauseMulAndPrint)
.reduce(Q.when, Q())
.done();
// ... 6 ... 2 ... 14
Promises handle single event very well
Extra complexity trying to handle sequence of events
var sleepSecond = _.partial(Q.delay, 1000);
var pauseMulAndPrint = function (n) {
return function () {
return sleepSecond()
.then(_.partial(byConstant, n))
.then(print);
};
};
numbers.map(pauseMulAndPrint)
.reduce(Q.when, Q())
.done();
// ... 6 ... 2 ... 14
Constructing a promise for each number :(
var sleepSecond = _.partial(Q.delay, 1000);
var pauseMulAndPrint = function (n) {
return function () {
return sleepSecond()
.then(_.partial(byConstantAsync, n))
.then(print);
};
};
"shoot the same bullet more than once"
Widely used in browser code
$(el).on('click', action) window.on('resize', cb) socket.on('message', handle)
We can extend the same approach to any event generator (like number sequence)
// action
var events = require('events');
var numberEmitter = new events.EventEmitter();
numberEmitter.on('number',
_.compose(print, byConstant)
);
Single code pipeline
var k = 0;
var ref = setInterval(function () {
numberEmitter.emit('number', numbers[k++]);
if (k >= numbers.length) {
clearInterval(ref);
}
}, 1000);
// prints 6, 2 14 with 1 second intervals
Goal: process number with 1 second pauses
numberEmitter
.on('number', action);
[3, 1, 7]
.map(mulBy2)
.forEach(print);
source of number events
.map(an async callback)
.buffer(n, async callback)
.forEach(another async callback);
Chained emitters
var stepEmitter = {
map: function (cb) {
var emitter = new events.EventEmitter();
return _.extend(emitter, stepEmitter);
},
forEach: function (cb) {
var emitter = new events.EventEmitter();
return _.extend(emitter, stepEmitter);
}
};
Chained emitters
var stepEmitter = {
map: function (cb) {
var emitter = new events.EventEmitter();
this.on('step', function (value) {
var mappedValue = cb(value);
emitter.emit('step', mappedValue);
});
return _.extend(emitter, stepEmitter);
},
forEach: function (cb) {
var emitter = new events.EventEmitter();
this.on('step', function (value) {
cb(value);
emitter.emit('step', value);
});
return _.extend(emitter, stepEmitter);
}
};
Chained emitters
var stepEmitter = {
map: function (cb) {
var emitter = new events.EventEmitter();
this.on('step', function (value) {
var mappedValue = cb(value);
emitter.emit('step', mappedValue);
});
return _.extend(emitter, stepEmitter);
},
forEach: function (cb) {
var emitter = new events.EventEmitter();
this.on('step', function (value) {
cb(value);
emitter.emit('step', value);
});
return _.extend(emitter, stepEmitter);
}
};
.map
emits cb(x)
.forEach
emits x
source of number events
.map(syncFn)
.forEach(syncFn);
Chained emitters
So far: steps are async, but each callback function is synchronous!
Async callback support via promises
var Q = require('q');
var stepEmitter = {
map: function (cb) {
var emitter = new events.EventEmitter();
this.on('step', function (value) {
Q.when(cb(value)).then(function (mappedValue) {
emitter.emit('step', mappedValue);
});
});
return _.extend(emitter, stepEmitter);
},
forEach: function (cb) {
var emitter = new events.EventEmitter();
this.on('step', function (value) {
Q.when(cb(value)).then(function () {
emitter.emit('step', value);
});
});
return _.extend(emitter, stepEmitter);
}
};
source of number events
.map(asyncFn)
.forEach(asyncFn);
Chained emitters + promises
async for the win!
var stepEmitter = {
// same map and forEach methods as above
// returns a new step emitter that accumulates N items
// emits the entire array with N items as single argument
buffer: function (n) {
var received = [];
var emitter = new events.EventEmitter();
this.on('step', function (value) {
received.push(value);
if (received.length === n) {
emitter.emit('step', received);
received = [];
}
});
return _.extend(emitter, stepEmitter);
}
};
source(numbers)
.map(byConstant)
.buffer(3)
.forEach(print);
// sleeps 3 seconds then prints [6, 2, 14]
What about buffering events?
source(numbers)
.map(byConstant) // data transform
.buffer(3) // sequence control
.forEach(print); // data transform
We implemented 2 types of operations on a sequence of events
.map, .filter
// vs
.buffer
Use a reactive FP library
var Rx = require('rx');
var timeEvents = Rx.Observable
.interval(1000)
.timeInterval(); // stream 1
var numberEvents = Rx.Observable
.fromArray(numbers); // stream 2
Rx.Observable.zip(timeEvents, numberEvents,
function pickValue(t, n) { return n; })
.map(byConstant)
.subscribe(print); // stream 3
// prints 6 2 14 with 1 second intervals
User click events
var Rx = require('rx');
var clickEvents = Rx.Observable
.fromEvent(document.querySelector('#btn'), 'click')
var numberEvents = Rx.Observable
.fromArray(numbers);
function pickSecond(c, n) { return n; }
Rx.Observable.zip(clickEvents, numberEvents,
pickSecond)
.map(byConstant)
.subscribe(print);
// prints a number on each button click
Marble diagram for
zip(s1, s2, fn)
function searchWikipedia (term) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term
}
}).promise();
}
Autocomplete
Hard problem: need to throttle, handle out of order returned results, etc.
function searchWikipedia (term) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term
}
}).promise();
}
var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
.map(function (e) { return e.target.value; })
.filter(function (text) { return text.length > 2; });
Autocomplete
keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.subscribe(function (data) {
// display results
});
Tell return values Bye-bye
// imperative code
var sum = add(2, 3);
// can use sum right away!
var multiplied = [1, 2, 3].map(mul);
// can use multiplied array right away!
var _ = require('lodash');
var byConstant = _.partial(mul, constant);
_(numbers)
.map(byConstant)
.forEach(print);
// we have not returned any values!
Tell middle functions Bye-bye
var byConstant = _.partial(mul, constant);
_(numbers)
.map(function (x) {
return byConstant(x);
})
.forEach(function (x) {
print(x);
});
_(numbers)
.map(byConstant)
.forEach(print);
// no middle functions
Tell deep equality checks Bye-bye*
var result = process(numbers);
if (deepEqual(result, numbers)) {
render();
}
var result = process(immutableNumbers);
if (result === immutableNumbers) {
render();
}
* with immutable data
const getTodo = (
id: number
): Effect.Effect<
unknown,
HttpClientError | TimeoutException
> =>
Http.request.get(`/todos/${id}`).pipe(
Http.client.fetchOk,
Http.response.json,
Effect.timeout("1 second"),
Effect.retry(
Schedule.exponential(1000).pipe(
Schedule.compose(Schedule.recurs(3)),
),
),
)
fetching data with retries and timeouts
By Gleb Bahmutov
JavaScript is an interesting language. It can mimic almost any style you want: procedural, object-oriented, functional, etc. In this presentation, I will take a simple problem and will solve it using different approaches. With each step, we will see the power of each approach to take the complexity away, while still being the JavaScript we all love to hate. Presented at iJS May 2024, 30 minutes
JavaScript ninja, image processing expert, software quality fanatic