Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
JS
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / FP FunP
JavaScript ninja, image processing expert, software quality fanatic
Purdue University
EveryScape
virtual tours
MathWorks
MatLab on the web
Kensho
finance dashboards
Around 50 conference decks at https://slides.com/bahmutov
with videos at https://glebbahmutov.com/videos
OSCON, ng-conf, NodeConf.eu ...
These slides at https://slides.com/bahmutov/pure-magic
link to more information
Usual E2E runners need delays or run slow in this case ... (Karma, Protractor, Selenium)
beforeEach(() => {
cy.visit('index.html')
})
it('starts with 0', () => {
cy.contains('Counter: 0')
})
it('increments counter 3 times', () => {
cy.contains('Increment').click()
cy.contains('Counter: 1')
cy.contains('Increment').click()
cy.contains('Counter: 2')
cy.contains('Increment').click()
cy.contains('Counter: 3')
})
Not single "delay" yet it runs in 6 seconds!
⏰ 40 min
⏰ 40 min
break
var numbers = [3, 1, 7]
var constant = 2
// 6 2 14
given an array of numbers, multiply each number by a constant and print the result.
var numbers = [3, 1, 7]
var constant = 2
// expected output [6, 2, 14]
const numbers = [3, 1, 7]
const constant = 2
let 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
const numbers = [3, 1, 7]
const constant = 2
reusable simple functions
complex logic
iteration
function mul(a, b) {
return a * b
}
function print(x) {
console.log(x)
}
function processNumber(n) {
print(mul(n, constant))
}
for(let k = 0; k < numbers.length; k += 1) {
processNumber(numbers[k])
}
// 6 2 14
function NumberMultiplier() {}
Object-Oriented JavaScript
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
// 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 () { ... }
}
OO JavaScript is prototypical
// instance object
{
numbers: [3, 1, 7]
}
prototype
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
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;
}
}
const n = new NumberMultiplier()
n.setNumbers([1, 2, 3])
setTimeout(n.print, 1000) // undefined
this will betray you
🔥🔥🔥
<div id="app-5">
<p>{{ message }}</p>
<button v-on:click="reverseMessage">Reverse</button>
</div>
new Vue({
el: '#app-5',
data: {
message: 'Hello Vue.js!'
},
methods: {
reverseMessage: function () {
this.message = this.message
.split('').reverse().join('')
}
}
})
this
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
this
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
"Smart" objects
Pass data around
"Dumb" objects
Pass logic around
new NumberMultiplier()
.setNumbers([3, 1, 7])
.multiply(2)
.print()
f(g(numbers))
typeof [1, 2] // "object"
Array.isArray([1, 2]) // true
Array.prototype
/*
map
forEach
filter
reduce
...
*/
Arrays: OO + FP
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
clear "multiply then print" semantics
Object-oriented methods
numbers
.map(x => mul(constant, x))
.forEach(print);
// 6 2 14
FP features
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
numbers
.map(x => mul(constant, x))
.forEach(print);
// 6 2 14
"dumb" object
pass logic as
arguments
When do we know arguments?
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(n) {
console.log(n);
}
numbers
.map(x => mul(constant, x))
.forEach(print);
// 6 2 14
known right away
known much later
Partial application
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
const mulBy = mul.bind(null, constant)
function print(n) {
console.log(n);
}
numbers
.map(x => mulBy(x))
.forEach(print);
// 6 2 14
Make new function
make code simpler
function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB)
newFn(valueC)
function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB);
// or
var _ = require('lodash');
var newFn = _.partial(fn, valueA, valueB);
// or
var R = require('ramda');
var newFn = R.partial(fn, valueA, valueB);
['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
function savePassword(userId, newPassword) {...}
// vs
function savePassword(newPassword, userId) {...}
Point-free code
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
const mulBy = mul.bind(null, constant)
function print(n) {
console.log(n);
}
numbers
.map(x => mulBy(x))
.forEach(print);
// 6 2 14
Point-free code
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
const mulBy = mul.bind(null, constant)
function print(n) {
console.log(n);
}
numbers
.map(mulBy)
.forEach(print);
// 6 2 14
single argument
Prepare for partial application
var numbers = [3, 1, 7];
var constant = 2;
const mul = a => b => a * b
const mulBy = mul(constant)
function print(n) {
console.log(n);
}
numbers
.map(mulBy)
.forEach(print);
// 6 2 14
These functions are different!
hint: look where the variables are coming from
function mul(a, b) {
return a * b
}
function print(x) {
console.log(x)
}
function mul(a, b) {
return a * b
}
function print(x) {
console.log(x)
}
These functions are different!
function mul(a, b) {
return a * b
}
only uses its arguments
same arguments => same result
does not modify the outside environment
function print(x) {
console.log(x)
}
uses outside variable
modifies the outside environment
function print(console, x) {
console.log(x)
}
['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
// function parseInt(x, radix)
// but Array.map sends (value, index, array)
['1', '2', '3'].map(/* ? */)
Write a FUNCTION that makes parseInt a unary function.
function unary() {
return function (x) {
return parseInt(x)
}
}
['1', '2', '3'].map(unary())
// [1, 2, 3]
unary strips second argument to parseInt
make unary a PURE function.
function unary(f) {
return function (x) {
return f(x)
}
}
['1', '2', '3'].map(unary(parseInt))
// [1, 2, 3]
unary is pure and reusable function
function unary(f) {
return function (x) {
return f(x)
}
}
['1', '2', '3'].map(unary(parseInt))
// [1, 2, 3]
functional JavaScript
Takes function as argument
Returns new function
initial spaghetti code
mul
...
readable code
"atoms"
...
initial spaghetti code
mul
short readable code
compose
...
...
for (k = 0; k < numbers.length; k += 1) {
print(byConstant(numbers[k]))
}
fn
fn
data
for (k = 0; k < numbers.length; k += 1) {
print(byConstant(numbers[k]))
}
fn
fn
data
mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
?
mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose()
mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g)
mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g) {
return function() {
}
}
mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g) {
return function(x) {
}
}
mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g) {
return function(x) {
return f(g(x))
}
}
const mulPrint = compose(print, byConstant)
for (k = 0; k < numbers.length; k += 1) {
mulPrint(numbers[k])
}
fn
data
function compose(f, g) {
return function(x) {
return f(g(x))
}
}
const mulPrint = compose(print, byConstant)
for (k = 0; k < numbers.length; k += 1) {
mulPrint(numbers[k])
}
fn
data
const F = compose(f, g)
F(x)
mulAndPrint = compose(print, byConstant)
print "pollutes" the composed function
function app(window) {
return function () {
window.title = 'Hello'
}
}
const run = app(window)
run()
// cannot declare argument const
function foo (a) {
a = 'whatever'
}
function foo (a) {
delete a.important
}
const b = {...}
foo(b) // 😎😎😎
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
_.map(array, cb)
Place on the left arguments that are likely to be known first
var R = require('ramda')
// callback is first argument!
R.map(cb, array)
// all functions are curried
var by5 = R.multiply(5)
by5(10) // 50
var R = require('ramda')
R.map(R.multiply(5), array)
// same as
R.map(R.multiply(5))(array)
const printAll = R.forEach(console.log)
printAll(multiplyAll(numbers))
const numbers = [3, 1, 7]
const constant = 2
const R = require('ramda')
const multiplyAll = R.map(R.multiply(constant))
const multiplyAll = R.map(R.multiply(constant))
const printAll = R.forEach(console.log)
printAll(multiplyAll(numbers))
fn
fn
data
// printAll(multiplyAll(numbers))
const computation = R.compose(printAll, multiplyAll)
computation (numbers)
Static logic
Dynamic data
const {pipe, map,
multiply, forEach} = require('ramda')
const mulPrint = pipe(
map(multiply(constant)),
forEach(console.log)
)
mulPrint (numbers)
Try to make JavaScript read just like English
My videos at
Awesome list of resources
<body>
<script src="https://unpkg.com/hyperapp@0.7.1"></script>
<script src="https://wzrd.in/standalone/hyperx"></script>
<script>
const {h, app} = hyperapp
const html = hyperx(h)
app({
model: 'World!',
view: (model) => html`<h2>Hello, ${model}</h2>`
})
</script>
</body>
pure view function
ES6 templates to VDOM
app({
model: 0,
actions: {
click: model => model + 1
},
view: (model, actions) => html`
<div>
<h2>${model}</h2>
<button onClick=${actions.click}>click</button>
</div>
`
})
pure view function
pure action functions
app({
model: 0,
actions: {
tick: model => model + 1,
reset: () => 0
},
subscriptions: [
(model, actions) =>
setInterval(actions.tick, 1000)
],
view: (model, actions) => html`
<div>
<h2>${model}</h2>
<button onClick=${actions.reset}>reset</button>
</div>
`
})
pure
pure
pure
not pure
view: (model, actions) => {
const queue = []
const schedule = queue.push.bind(queue)
const go = _ => {
queue.forEach(f => f())
queue.length = 0
}
return html`
<div>
<h2>${model}</h2>
<button onClick=${_ => schedule(actions.click)}>
schedule</button>
<button onClick=${go}>go</button>
</div>
`
}
var numbers = [1, 2, 3]
numbers.forEach(print)
// 1, 2, 3
numbers.push(100)
// nothing happens
numbers = Rx.Observable.create(observer => {
})
numbers.subscribe(print)
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
// ... after long delay
observer.onNext(100)
observer.onCompleted()
// 1, 2, 3
// ...
// 100
// creation
var fn = function () { ... }
var ob = Rx.Observable.create(...)
// running
fn() // or fn.call()
numbers.subscribe(...)
var ob = Rx.Observable.create(...)
numbers.subscribe(onNext, onError, onEnd)
// stream with error
// ----1---2---3----X
// stream with end event
// --1---2-3----|
// ----1---2------3----|
// .map(double)
// ----2---4------6----|
// .filter(x => x > 4)
// ---------------6----|
[1, 2, 3]
.map(double) // [2, 4, 6]
.filter(x => x > 4) // [6]
.forEach(print) // 6
Rx.from([1, 2, 3]) // Observable 1
.map(double) // Observable 2
.filter(x => x > 4) // Observable 3
.subscribe(print) // 6
must subscribe to cold
stream to start the event flow!
Rx.from([1, 2, 3])
.map(double)
.buffer(3)
.delay(5)
.subscribe(print)
// pause of 5 seconds
// [2, 4, 6]
var numbers = Rx.from([1, 2, 3])
var clicks = Rx.Observable.fromEvent(
document.querySelector('#btn'), 'click')
// -1-2-3-| numbers
// --c-------c-c------ clicks
// zip((number, c) => number)
// --1-------2-3-|
numbers
.zip(clicks, (number, t) => number)
.subscribe(print)
function searchWikipedia (term) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term
}
}).promise();
}
Hard problem: need to throttle, handle out of order returned results, etc.
var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.subscribe(function (data) {
// display results
});
How?
var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.subscribe(function (data) {
$('#results').innerHTML = '<ul>' +
data.map(x => `<li>${x}</li>`) +
'</ul>'
});
Slow if only part of data changes
var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.subscribe(function (data) {
var vtree = vdom.ul(data.map(vdom.li))
vdom.patch($('#results'), vtree)
});
widely used, fast
light
fastest
tiny
function main() {
var keyups = Rx.Observable.fromEvent(
$('#input'), 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.subscribe(function (data) {
var vtree = vdom.ul(data.map(vdom.li))
vdom.patch($('#results'), vtree)
});
}
main()
Input from DOM
Output to DOM
function main() {
var keyups = Rx.Observable.fromEvent(
$('#input'), 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
return keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.map(data => vdom.ul(data.map(vdom.li))
}
function run(fn) {
fn().subscribe(function (vtree) {
vdom.patch($('#results'), vtree)
});
}
run(main)
Input from DOM
Output to DOM
function main(dom$) {
var keyups = dom$('#input', 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
return keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.map(data => vdom.ul(data.map(vdom.li))
}
function run(fn) {
var dom$ = (sel, event)
=> Rx.Observable.fromEvent(...) }
fn(dom$).subscribe(function (vtree) {
vdom.patch($('#results'), vtree)
});
}
run(main)
Input and Output
pure app
function main(dom$) {
var keyups = dom$('#input', 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
return keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
.map(data => vdom.ul(data.map(vdom.li))
}
function run(fn) {
var dom$ = (sel, event)
=> Rx.Observable.fromEvent(...) }
fn(dom$).subscribe(function (vtree) {
vdom.patch($('#results'), vtree)
});
}
run(main)
cycle.js framework
pure app
function main({DOM}) {
const decrement$ = DOM.select('.decrement')
.events('click').map(ev => -1);
const increment$ = DOM.select('.increment')
.events('click').map(ev => +1);
const action$ = Observable.merge(decrement$, increment$);
const count$ = action$.startWith(0).scan((x,y) => x+y);
const vtree$ = count$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
return { DOM: vtree$ };
}
ui to intent
intent to model
model to view
function main({DOM}) {
const decrement$ = DOM.select('.decrement')
.events('click').map(ev => -1);
const increment$ = DOM.select('.increment')
.events('click').map(ev => +1);
const action$ = Observable.merge(decrement$, increment$);
const count$ = action$.startWith(0).scan((x,y) => x+y);
const vtree$ = count$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
return { DOM: vtree$ };
}
every function
including 'main'
is pure
CycleConf2017 videos https://vimeo.com/album/4578937
JS
By Gleb Bahmutov
There are a few web frameworks that are all the rage right now: React, VueJs, but which frameworks are functional AND reactive? Dr. Gleb Bahmutov will talk about functional reactive web applications you can write today, and why they are going to rock tomorrow. Presented at Maine.JS meetup in May of 2017
JavaScript ninja, image processing expert, software quality fanatic