Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
http://slides.com/bahmutov
Cycle.js is conceptually maybe the best framework currently. ... But for me it is really hard to write and understand. Too hard actually.
Node, C++, C#,
Angular, Vue.js, React, server-side etc.
event analysis and statistics for financial markets
Boston and New York
const todos = getTodos()
const updatedTodos = addNewTodo(todos, newTodo)
saveTodos(updatedTodos)
where does it get the data?
how does it save the data?
is this object modified?
const todos = getTodos()
const updatedTodos = addNewTodo(todos, newTodo)
saveTodos(updatedTodos)
what if getTodos() is called again
while the first save is still executing?
const todos = getTodos()
const updatedTodos = addNewTodo(todos, newTodo)
saveTodos(updatedTodos)
functional: A way to write your program in terms of simple functions
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
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
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
multiplies two numbers
prints one argument
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
const mulBy = mul.bind(null, constant)
// mulBy(x) same as mul(constant, x)
partial application
var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
const mulBy = mul.bind(null, constant)
// mulBy(x) same as mul(constant, x)
function mulThenPrint(x) {
print(mulBy(x))
}
fn1
fn2
x
const mulBy = mul.bind(null, constant)
// function mulThenPrint(x) {
// print(mulBy(x))
// }
const R = require('ramda')
const mulThenPrint = R.compose(print, mulBy)
fn1
fn2
Where is data variable "x"?
function mulThenPrintOld(x) {
print(mulBy(x))
}
const mulThenPrint = R.compose(print, mulBy)
function mulThenPrintOld(x) {
mulThenPrint(x)
}
local variable name;
can be called anything
function mulThenPrintOld(x) {
print(mulBy(x))
}
const mulThenPrint = R.compose(print, mulBy)
function mulThenPrintOld(dragons) {
mulThenPrint(dragons)
}
outer function does nothing but calls inner one
function mulThenPrintOld(x) {
print(mulBy(x))
}
const mulThenPrint = R.compose(print, mulBy)
- 1 function
- 1 variable
var numbers = [3, 1, 7];
var constant = 2;
reusable simple functions
complex logic
iteration
function mul(a, b) {
return a * b;
}
function print(x) {
console.log(x);
}
const mulBy = mul.bind(null, constant)
const mulThenPrint = R.compose(print, mulBy)
for(k = 0; k < numbers.length; k += 1) {
mulThenPrint(numbers[k]);
}
// 6 2 14
var numbers = [3, 1, 7]
var constant = 2
for(k = 0; k < numbers.length; k += 1) {
mulThenPrint(numbers[k])
}
numbers.forEach(mulThenPrint)
numbers
.map(mulBy)
.forEach(print)
clear semantics
same result
[3, 1, 7]
.map(x => x * 2)
.filter(x > 10)
.map(x => x + 10)
.forEach(print)
// 24
// [3, 1, 7]
// [6, 2, 14]
// [14]
// [24]
// undefined
Each call (.map .filter) returns another array
Each call takes a function as an argument
[3, 1, 7]
.map(x => x * 2)
.filter(x > 10)
.map(x => x + 10)
.forEach(print)
// 24
// [3, 1, 7]
// [6, 2, 14]
// [14]
// [24]
// undefined
function mul(a, b) {
return a * b
}
function print(x) {
console.log(x)
}
not pure - console is declared outside
function print(x) {
console.log(x)
}
not pure - leaves side effects in terminal
[1, 2, 3].map(x => x * 2)
.forEach(console.log)
Error: Illegal invocation
[1, 2, 3].map(x => x * 2)
.forEach(console.log.bind(console))
[1, 2, 3].map(double)
// [2, 4, 6]
window.postMessage(double.toString())
const recreated = eval('(' + message + ')')
function double(x) {
return x * 2
}
const F = R.compose(f, g, h)
F(x)
var A = 10
var B = 20
var C = A + B
// C is 30
A = -5
// C is 30
var A = 10
var B = 20
var C = A + B
// C is 30
I want C to be recomputed when A or B changes
var C = A + B
Set C to value of A + B
???
C is sum of A + B
A = -5
C = A + B
// C is 15
We need to call "C = ..." to set it to the new value
A
B
C
Every code updating A or B has to also update C
A
B
C
Ties A and B to C very closely
A
B
C
A
B
C
C knows that it needs to update itself when A or B changes
A
B
C
A and B emit an event;
C updates when event arrives
-5
3
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 seconds = Rx.Observable
.interval(1000).timeInterval()
numbers
.zip(seconds, (number, s) => number)
.subscribe(print)
// one second pause
// 1
// ... one second later
// 2
// .... one second later
// 3
var numbers = Rx.from([1, 2, 3])
var seconds = Rx.Observable
.interval(1000).timeInterval()
numbers
.zip(seconds, (number, s) => number)
.subscribe(print)
// -1-2-3-| numbers
// ----s----s----s---- seconds
// zip((number, s) => number)
// ----1----2----3-|
var numbers = Rx.from([1, 2, 3])
var clicks = Rx.Observable.fromEvent(
document.querySelector('#btn'), 'click')
numbers
.zip(clicks, (number, t) => number)
.subscribe(print)
// prints 1 number every time
// element "#btn" is clicked
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)
});
the hard stuff: async data and action flow
the simple part: updating UI
widely used, fast
hipster
light
fastest
tiny, for Cycle
data
finished?
$http.get(...).then(...)
finished?
$http.get(...).then(...)
.then(...)
* times
stream
stream operation
* times
stream
stream
stream
RxJS for the win!
stream
stream
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)
DOM events
cycle.js framework
pure app
function main({DOM, HTTP, WebSockets, DB}) {
var keyups = DOM.select('#input', 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
var results = keyups.throttle(500)
.distinctUntilChanged()
.flatMapLatest(searchWikipedia)
return {
DOM: results.map(data
=> vdom.ul(data.map(vdom.li)),
HTTP: // Ajax request stream,
WebSockets: // stream output to channel,
DB: // stream of DB updates
}
}
import {makeDOMDriver} from '@cycle/dom'
import {makeHTTPDriver} from '@cycle/http'
import {makeCookieDriver} from 'cyclejs-cookie'
import {makeTermDriver} from 'cycle-blessed'
const drivers = {
DOM: makeDOMDriver('#app'),
HTTP: makeHTTPDriver({baseUrl: '...'}),
cookie: makeCookieDriver(),
term: makeTermDriver(blessed.screen())
};
run(main, drivers)
RxJS most.js xstream
snabdom virtual-dom
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$ };
}
function main({DOM}) {
const decrement$ = DOM.select('.decrement')
.events('click').map(ev => -1);
const increment$ = DOM.select('.increment')
.events('click').map(ev => +1);
}
ui to intent
// --- click ---- click -----
// .map(ev => -1)
// --- -1 ------- -1 -------- decrement$
// -- click ------ click ---
// .map(ev => +1)
// -- 1 ---------- 1 -------- .increment
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);
}
ui to intent
intent to model
// ---- -1 ----------------------------
// ------------ 1 ------- 1 --- 1 -----
// .merge()
// ---- -1 ---- 1 ------- 1 --- 1 -----
// .scan((x, y) => x + y)
// ---- -1 ---- 0 ------- 1 --- 2 -----
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
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
reactive Redux
e
e
e
e
e
e
e
e
e
X
e
run(main, drivers)
First ever CycleConf (April 2016)
André Staltz @andrestaltz
Nick Johnstone @widdnz
(not pictured) Tylor Steinberger
Uses functional reactive streams to describe the data flow in application
Side effects (DOM, HTTP, etc) are isolated via drivers
Works out of the box with real time backends (WebSockets, IoT)
presentation.subscribe(null, null, thankAudience)
presentation.onCompleted()
Thank you!
By Gleb Bahmutov
Cycle.js - functional reactive framework for pure web apps. Presented at FrontEndCamp at United Nations NYC July 2016. Links and additional resources at https://glebbahmutov.com/blog/cyclejs-frontendcamp/
JavaScript ninja, image processing expert, software quality fanatic