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