A journey in the world of
Reactive Programming
with JavaScript
Seminal paper:
Functional Reactive Animation (Fran),
Conal Elliott and Paul Hudak, 1997
Composer — Rich SPA in Angular
Plumber — Declarative build pipelines
Media service — Reactive Scala channels
=
“Treat computation as the evaluation of mathematical functions and avoid state and mutable data.”
=
“[Pure functions] produce results that depend only on their inputs and not on the program state.”
Avoid side-effects
No statements, only expressions
Avoid side-effects: Immutable values
Don’t mutate: transform, combine, yield new values
Simple data types: collections, list, map
Find length of longest Guardian URL in a list
var urls = [
"http://www.theguardian.com/world/2014/apr/22/north-koreans-turning-against-the-regime",
"http://www.dailymail.co.uk/news/article-2610685/Dont-ask-migrants-long-theyre-staying-Border-guards-told-quizzing-arrivals-EU-breaches-Brussels-rules.html",
"http://www.theguardian.com/lifeandstyle/wordofmouth/2014/mar/10/quinoa-cappuccino-soy-almond-oat-milk-dairy-alternatives"
];
// Imperative
var maxGuUrlLen = 0;
for (var i = 0, l = urls.length; i < l; i++) {
if (/^https?:\/\/www.theguardian.com/.test(urls[i])) {
maxGuUrlLen = Math.max(maxGuUrlLen, urls[i].length);
}
}
// Functional with composition
var maxGuUrlLen = urls.filter(function(s) {
return /^https?:\/\/www.theguardian.com/.test(s);
}).map(function(s) {
return s.length;
}).reduce(function(a, b) {
return Math.max(a, b);
}, 0);
// Define helpers
var guardianRegex = /^https?:\/\/www.theguardian.com/;
var isGuardianUrl = guardianRegex.test.bind(guardianRegex);
function prop(name) {
return function(obj) {
return obj[name];
};
}
function max(a, b /* ignore the rest */) {
return Math.max(a, b);
}
// Imperative
var maxGuUrlLen = 0;
var len = prop("length");
for (var i = 0, l = urls.length; i < l; i++) {
if (isGuardianUrl(urls[i])) {
maxGuUrlLen = max(maxGuUrlLen, len(urls[i]));
}
}
// Functional with composition
var maxGuUrlLen = urls.
filter(isGuardianUrl).
map(prop("length")).
reduce(max);
More readable
More reusable
Separate logical tasks
Like Lego blocks, rather than Jenga tower
“A style of building the structure and elements of computer programs, that expresses the logic of a computation without describing its control flow.”
Express the result in terms of the inputs, rather than starting from the input and crafting your way to the result
Express what the program should do,
not how
+
Some things take time
JavaScript is single-threaded: don’t wait!
> ajax(url, function(err, content) { ... });
// => undefined
Based on side-effects
var urls = [...];
urls.
filter(isGuardianUrl).
map(function(url) {
ajax(url, function(err, content) {
// ???
});
});
Not composable
ajax(url, function(err, content) {
if (! err) {
ajax(sequencedUrl, function(err2, content2) {
});
}
});
ajax(parallelUrl, function(err3, content3) {
});
No sequencing or concurrency
function getAsync(cb) {
ajax(url, function(err, content) {
if (err) return cb(err);
// ...
});
}
No automatic error propagation
Callbacks = fallback to imperative code
var url = "http://api.example.com/article/1";
var response = $http.get(url);
response.then(function(body) {
console.log("received response: ", body);
}, function() {
console.error("Oops, ajax failed");
});
Represent the eventual value (or error) over time
then()
Not a way to register callbacks
Transforms / maps the eventual value
var headline = response.then(function(body) {
return body.field.headline;
});
Note: you can’t “extract” the value, you must receive it as part of a then()
var authorName = response.then(function(body) {
return body.author.id;
}).then(function(authorId) {
return $http.get("/api/people/" + authorId);
}).then(function(author) {
return author.name;
}, function(e) {
console.log("epic fail:", e);
});
var authorName;
try {
var authorId = body.author.id;
var author = get("/api/people/" + authorId);
authorName = author.name;
} catch(e) {
console.log("epic fail:", e);
}
Promises guide us through the “happy path”
var urls = [ ... ];
var requestPromises = urls.
filter(isGuardianUrl).
map(ajax);
var maxGuPageLen = Promise.all(requestPromises).
then(function(pages) {
return pages.map(prop("length")).reduce(max);
});
Like async, but initiated externally
if (document.readyState === "complete") {
doTheThing();
} else {
document.addEventListener("readystatechange",
function observe() {
if (document.readyState === "complete") {
doTheThing();
document.removeEventListener("readystatechange",
observe);
}
});
}
var domReady = new Promise(function(resolve, reject) {
if (document.readyState === "complete") {
resolve();
} else {
document.addEventListener("readystatechange",
function observe() {
if (document.readyState === "complete") {
resolve();
document.removeEventListener("readystatechange",
observe);
}
});
}
});
domReady.then(doTheThing);
// Elsewhere
domReady.
then(loadComments).
then(highlightComments);
Uniform interface for delayed values
It’s not about avoiding “callback hell”
Promise#then
)Promise.all
)Libraries: when.js, rsvp, Q, Angular $q, Bluebird, etc.
Native in ES6,
already available in modern browsers,
soon in NodeJS
Like eventual state, but ever-changing
<input>
valuevar queryEl = document.querySelector(".query");
var q = queryEl.value;
queryEl.addEventListener("input", function(ev) {
q = queryEl.value;
});
var q = queryEl.value;
var cleanQ = q.trim().toLowerCase();
var entries = matchEntries(cleanQ);
// ... more things with q...
queryEl.addEventListener("input", function(event) {
q = queryEl.value;
cleanQ = q.trim().toLowerCase();
entries = matchEntries(cleanQ);
// ... more things with q...
});
What about the code that uses entries
?
Anything that depends on a mutable state is itself mutable
Represent that dependency declaratively
$scope.firstName = "Conal";
$scope.lastName = "Elliot";
$scope.fullName = function() {
return $scope.firstName + " " + $scope.lastName;
};
// Read current computed value
$scope.fullName(); // "Conal Elliot"
// Observe computed value for changes
$scope.$watch("fullName()", function(fullName) {
console.log("Hello " + fullName);
});
// outputs "Hello Conal Elliot"
// Update source value
$scope.firstName = "Billy";
// outputs "Hello Billy Elliot"
// on next $digest cycle
Expression as a simple function
Must observe using $watch
Expensive dirty-checking
(until Object.observe
)
$watch
uses Object reference,
need to use $watchCollection
App.Person = Ember.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
return this.get("firstName") + " " +
this.get("lastName");
}.property("firstName", "lastName")
});
var person = App.Person.create({
firstName: "Conal",
lastName: "Elliot"
});
person.get("fullName"); // "Conal Elliot"
App.Person = Ember.Object.extend({
// ...
fullNameChanged: function() {
console.log("Hello " + this.get("fullName"));
}.observes("fullName").on("init")
});
person.set("firstName", "Billy");
// outputs "Hello Billy Elliot"
Explicit deps
Only as part of Ember.Object
class
Weird syntax for collections
.property("todos.@each.isDone")
var firstName = ko.observable("Conal");
var lastName = ko.observable("Elliot");
var fullName = ko.computed(function() {
return firstName() + " " + lastName();
});
fullName(); // "Conal Elliot"
fullName.subscribe(function(fullName) {
console.log("Hello " + fullName);
});
// outputs "Hello Conal Elliot"
firstName("Billy");
// outputs "Hello Billy Elliot"
Simple type to wrap mutable values
Can be passed around like any value
Automatic dependencies
Always synchronous
var friends = ko.computed(function() {
asyncFindFriends(fullName(), function(err, ret){
// ... ??
});
return // ... ??
});
var friends = ko.computed(function() {
return promiseFindFriends(fullName());
});
// Now friends() returns a Promise...
var friendOfFriends = ko.computed(function() {
return friends().then(function(frs) {
return Promise.all(frs.map(function(f) {
return promiseFindFriends(f);
})).map(flatten);
});
});
// Now friendOfFriends() returns a Promise too...
We need flatMap!
“The basic idea behind reactive programming is that there are certain data types that represent a value ‘over time’. Computations that involve these changing-over-time values will themselves have values that change over time.”
// Given an EventStream `query'...
var matchingTitle = query.
map(function(q){ return q.trim().toLowerCase();}).
flatMap(function(q) {
var request = reqwest({
url: "http://content.guardianapis.com/search",
type: "jsonp",
data: {"page-size": 1, q: q}
});
return Bacon.fromPromise(request);
}).map(function(resp) {
return resp.response.results[0].webTitle;
});
var query = Bacon.
fromEventTarget(el, "input").
map(".target.value");
var query = Bacon.
fromEventTarget(webSocket, "message").
map(".data")
var matchingTitle = query.
map(function(q){ return q.trim().toLowerCase();}).
filter(function(q){ return q.length > 2; }).
flatMap(function(q) { // ...
var lastTenMatchingTitles =
matchingTitle.slidingWindow(10);
// Each value is an Array of titles
<button id="plus" >+</button>
<button id="minus">-</button>
<input type="text" id="number" />
var plusButton = document.getElementById("plus");
var minusButton = document.getElementById("minus");
var plus = Bacon.
fromEventTarget(plusButton, "click").map(1);
var minus = Bacon.
fromEventTarget(minusButton, "click").map(-1);
var number = plus.merge(minus).
scan(0, function(a,b) { return a + b });
var numberField = document.getElementById("number");
number.onValue(function(n) {
numberField.value = n; // side-effect
});
Similar model, more modular
API similar to other languages (incl. Scala): Observable, Observers, Subjects, Schedulers, etc.
Querying of async data streams (LINQ)
Poor documentation
Lazy streams, concurrency, backpressure
var filenames = highland(["foo.txt", "bar.txt", "baz.txt"]);
var extensions = filenames.map(function(filename){
return filename.split(".")[1];
});
// doesn't actually map anything yet
extensions.each(function(extension) {
console.log(extension);
}); // causes the stream to resume and output
var i = 0;
var integers = highland(function(push, next) {
push(null, i++);
next();
});
integers.take(10).each(function(i) {
console.log(i);
});
// Outputs 0, 1, 2, 3 ... 10
var readFile = highland.wrapCallback(fs.readFile);
// readFile: String => Stream[Buffer]
var filenames = highland(["foo.txt", "bar.txt", "baz.txt"]);
var fileReaders = filenames.map(readFile);
// Stream[Stream[Buffer]] -- Nothing has run yet
var fileContents = fileReaders.parallel(10);
// Stream[Buffer] -- Nothing has run yet
filenames.observe().zip(fileContents).
each(function(pair) {
console.log(pair[0], ":", pair[1].toString());
});
stream.parallel(n);
Consume n
streams at a time, buffering to retain order.
stream.series();
Consume one stream at a time in order.
stream.merge();
Consume all streams at once, returning them as soon as they arrive (any ordering).
Simpler conceptually
Avoid flooding slow consumers by managing the source
Both data input and output
Render initial UI from model
Re-render UI on model change event
Update model on UI change event
Backbone.View.extend({
template: _.template(...),
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
render: function() {
var data = this.model.attributes;
this.$el.html(this.template(data));
this.$(".delete").toggle(user.isAdmin);
return this;
}
});
Not declarative, render as a side-effect
Expensive re-render, not targetted
Ideally, Viewt = f(Modelt)
<ul ng-repeat="task in tasks">
<li ng-class="{'done': isDone() || expired}">
{{name}}
</li>
</ul>
<footer ng-show="tasks.length > 0">
Total: {{tasks.length}}
</footer>
Declarative bindings (incl. expressions)
No imperative DOM manipulations
getInitialState: function() {
return {salutation: "Hello!"};
},
handleChange: function(event) {
this.setState({salutation: event.target.value});
},
render: function() {
var value = this.state.salutation;
return <input type="text" value={value}
onChange={this.handleChange} />;
}
Side-effects, boilerplate
<input type="text" data-bind="value: salutations"/>
<script>
var model = {
salutations: ko.observable()
};
ko.applyBindings(model);
</script>
Model value updated on view change
Angular, Knockout, Ember, Ractive, Ripples, etc.
Bind computed or inline expressions into the view
Bind view value to a model value (bidirectional)
Bind Observable into the view
Bind view value to an Observable
Hybrid: angular-bacon, rx.angular
<input type="text" ng-model="username"/>
<input type="submit" ng-disabled="formValid"/>
// Controller:
var username = $scope.$watchAsProperty("username");
var password = $scope.$watchAsProperty("password");
var passwordIsValid = password.map(longerThan(5));
var usernameIsFree = username.changes().
flatMapLatest(asyncCheckUsername).
merge(username.map(false)).
toProperty(false);
usernameIsFree.and(passwordIsValid).
digest($scope, "formValid");
Purely declarative
Including async and mutable state
Express the result in terms of the inputs
}
=
Reactive: extension of functional principles to include mutable state and time (async, events)
Observable data types to represent values over time and isolate side-effects
Reactive principles can be borrowed in a variety of contexts
Use a spacebar or arrow keys to navigate