Cycle.js: Get in the Loop
http://slides.com/bahmutov
A functional and reactive JavaScript framework for cleaner code
A weird and difficult JavaScript framework for driving you crazy
- Angular1
- Angular2
- Ember
- Cycle.js
- React with Redux
Defining components in
Cycle.js is conceptually maybe the best framework currently. ... But for me it is really hard to write and understand. Too hard actually.
what is
- functional ?
- reactive ?
- Cycle.js ?
- difficult about it ?
Dr. Gleb Bahmutov PhD
Node, C++, C#,
Angular, Vue.js, React, server-side etc.
work: Kensho
event analysis and statistics for financial markets
Boston and New York
what is
- functional ?
- reactive ?
- Cycle.js ?
- difficult about it ?
How do I write great apps?
simplicity & clarity
If I can read the source code
and understand what happens
then most likely it will work
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
functions everywhere
good functions
-
small
-
simple
-
pure
functional refactoring
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
procedural / imperative JS
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
simple
a function does one thing only
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
a function does one thing only
making new functions
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
composition
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
composition
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"?
point free code
function mulThenPrintOld(x) {
print(mulBy(x))
}
const mulThenPrint = R.compose(print, mulBy)
function mulThenPrintOld(x) {
mulThenPrint(x)
}
local variable name;
can be called anything
point free code
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
point free code
function mulThenPrintOld(x) {
print(mulBy(x))
}
const mulThenPrint = R.compose(print, mulBy)
- 1 function
- 1 variable
Refactoring
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
Iteration
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
Iteration
[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
Iteration
[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
Array is the data structure
which functions make the best callbacks?
Pure functions
function mul(a, b) {
return a * b
}
- only uses its input arguments
- does not modify anything outside
- same inputs => same result every time
Pure functions
function print(x) {
console.log(x)
}
- only uses its input arguments
- does not modify anything outside
- same inputs => same result every time
not pure - console is declared outside
Pure functions
function print(x) {
console.log(x)
}
- only uses its input arguments
- does not modify anything outside
- same inputs => same result every time
not pure - leaves side effects in terminal
Methods usually make poor callbacks (use "this")
[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 + ')')
Pure functions are portable
function double(x) {
return x * 2
}
const F = R.compose(f, g, h)
F(x)
pure
pure
Pure functions are the best
Simple to write
Simple to read
Simple to test
My advice
-
Write small simple functions
-
compose them
-
Progress slowly through the code base
what is
- functional ?
- reactive ?
- Cycle.js ?
- difficult about it ?
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
imperative
A
B
C
Every code updating A or B has to also update C
imperative
A
B
C
Ties A and B to C very closely
reactive: spreadsheet
reactive: spreadsheet
imperative
A
B
C
reactive
A
B
C
C knows that it needs to update itself when A or B changes
reactive
A
B
C
A and B emit an event;
C updates when event arrives
-5
3
What are "A" & "b"?
Event Emitters
observables
Custom code
observables
are like infinite arrays
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
observables are like functions that can return multiple values
// creation
var fn = function () { ... }
var ob = Rx.Observable.create(...)
// running
fn() // or fn.call()
numbers.subscribe(...)
success, error, end
var ob = Rx.Observable.create(...)
numbers.subscribe(onNext, onError, onEnd)
// stream with error
// ----1---2---3----X
// stream with end event
// --1---2-3----|
diagrams
// ----1---2------3----|
// .map(double)
// ----2---4------6----|
// .filter(x => x > 4)
// ---------------6----|
diagrams: marbles
operators
[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!
~175 RxJS operators
Rx.from([1, 2, 3])
.map(double)
.buffer(3)
.delay(5)
.subscribe(print)
// pause of 5 seconds
// [2, 4, 6]
stream combinators
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
stream combinators
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-|
User events
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
User events
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();
}
Write autocomplete
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)
Autocomplete with RxJs
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)
display results
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)
use virtual dom
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
reactive streams
RxJs
Bacon.js
Kefir
Most.js
xstream
your choice
widely used, fast
hipster
light
fastest
tiny, for Cycle
Kensho dashboard app
data
Kensho dashboard app
Kensho dashboard app
finished?
$http.get(...).then(...)
Kensho dashboard app
finished?
$http.get(...).then(...)
.then(...)
* times
Kensho dashboard app
stream
stream operation
* times
stream
stream
stream
RxJS for the win!
stream
stream
what is
- functional ?
- reactive ?
- Cycle.js ?
- difficult about it ?
remember pure functions?
cycle.js makes your application pure
factor out side effects
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
step 1: separate output
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
step 2: separate input
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
step 3: cycle events
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
cycle.js is a tiny layer for controlling side effects ON TOP OF REACTIVE STREAMS
your app's "main" connects sources to sinks using stream operators
sources to sinks streams
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
}
}
drivers make sources & sinks
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)
stream lib
VDOM lib
cycle.js diversity
RxJS most.js xstream
snabdom virtual-dom
counter example
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$ };
}
counter example
function main({DOM}) {
const decrement$ = DOM.select('.decrement')
.events('click').map(ev => -1);
const increment$ = DOM.select('.increment')
.events('click').map(ev => +1);
}
counter example
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);
}
counter example
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$ };
}
counter example
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$ };
}
counter example
ui to intent
intent to model
model to view
reactive Redux
what is
- functional ?
- reactive ?
- Cycle.js ?
- difficult about it ?
main runs only once
events start flowing
e
e
e
e
e
e
e
e
e
X
e
run(main, drivers)
Build Rx stream
and watch events flow
cycle forces your to think about everything that can EVER happen
everything
learning cycle.js
Cycle docs are fantastic
Cycle community is friendly and helpful
learning cycle.js
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
Cycle.js
Works out of the box with real time backends (WebSockets, IoT)
Cycle.js
presentation.subscribe(null, null, thankAudience)
presentation.onCompleted()
Thank you!