Writing elegant JavaScript
with Functional Reactive Programming
Jimbo Jones
263 6th St., Suite 206
Springfield, NT 01646
Hours: 8 AM - 5 PM
Tel: 800-267-8634, ext 82
Seymore Skinner
263 6th St., Suite 206
Springfield, NT 01646
Hours: 11 AM - 7 PM
Tel: 800-267-8634, ext 59
Carl Carlton
2354 N. President St.
Springfield, NT 01649
Hours: 9 AM - 6 PM
Tel: 877-236-0224
For every visitor to our site, display a list of five sales agents. Only show agents with open office hours. Of those, show the ones that are closest to the visitor's location first. For sales agents that share an office, list them in random order so they get equal chances for commissions.
Algorithm:
-
Create an empty Array A.
-
Loop through the reps.
-
If a rep is not currently available, go to the next one.
-
Calculate the distance between the customer and rep's locations.
-
Store the rep with that distance in the array along with a random number.
-
-
When finished looping, sort the array by distance & the random number.
-
Create a new Array B.
-
Loop 5 times, with index i.
-
For the rep at index i of A, place the contact information into B.
-
-
Return B.
function getSalesContacts(allReps, max, location) {
var available = [], rep;
for (var i = 0; i < allReps.length; i++) {
rep = allReps[i];
if (!rep.isAvailable()) {
continue;
}
available.push({
rep: rep,
dist: getDistance(location, rep.location),
rand: Math.random()
});
}
available.sort(function (a, b) {
return (a.dist - b.dist) || (a.rand - b.rand);
});
var results = [];
for (i = 0; i < Math.min(max, available.length); i++) {
rep = available[i].rep;
results.push({
name: rep.name,
phone: rep.phone,
email: rep.email,
location: rep.location,
hours: rep.hours
});
}
return results;
}
For every visitor to our site, display the contact information of five sales agents. Only show agents with open office hours. Of those, show the ones that are closest to the visitor's location first. For sales agents that share an office, list them in random order so they get equal chances for commissions.
Filter
For every visitor to our site, display the contact information of five sales agents. Only show agents with open office hours. Of those, show the ones that are closest to the visitor's location first. For sales agents that share an office, list them in random order so they get equal chances for commissions.
Map
For every visitor to our site, display the contact information of five sales agents. Only show agents with open office hours. Of those, show the ones that are closest to the visitor's location first. For sales agents that share an office, list them in random order so they get equal chances for commissions.
Sort
For every visitor to our site, display the contact information of five sales agents. Only show agents with open office hours. Of those, show the ones that are closest to the visitor's location first. For sales agents that share an office, list them in random order so they get equal chances for commissions.
Slice
function getSalesContacts(allReps, max, location) {
return allReps
.filter(rep => rep.isAvailable())
.map(rep => ({
rep: rep,
dist: getDistance(location, rep.location),
rand: Math.random()
}))
.sort((a, b) => (a.dist - b.dist) || (a.rand - b.rand))
.slice(0, max)
.map(d => {
rep = d.rep;
return {
name: rep.name,
phone: rep.phone,
email: rep.email,
location: rep.location,
hours: rep.hours
}
});
}
function getSalesContacts(allReps, max, location) {
return _(allReps)
.filter(rep => rep.isAvailable())
.map(rep => ({
rep: rep,
dist: getDistance(location, rep.location),
rand: Math.random()
}))
.sortByAll(['dist', 'rand'])
.take(max)
.pluck('rep')
.pick(['name', 'phone', 'email', 'location', 'hours']);
}
Elegant Code is...
-
concise
-
Clear
-
Consistent
Asynchronous Code:
JavaScript's Greatest Strength & Weakness
What it gets right
-
Non-blocking execution (besides alert, confirm, etc)
-
Single-threaded; uses event loop instead of parallel execution, so no need for locking primitives to protect mutable state.
-
Parallel code (Web Workers, etc) shares no mutable state; communication through message passing (Actor pattern)
-
Function expressions enable easy definition of callbacks.
What's missing?
- Consistency
- Composibility
Asynchronous Functions:
Arbitrary Callbacks
$.ajax({
url: 'http://some.url/',
success: function (data, status, xhr) {
// Handle success
},
error: function (xhr, status, err) {
// Handle error
}
});
Asynchronous Functions:
Node-style Callbacks
request.get('http://some.url/', function (err, result, body) {
if (err) {
// Handle error
} else {
// Handle result
}
});
Asynchronous Functions:
Christmas Tree of Doom
function doDependentHttpRequests(cb) {
request.get('http://some.url/a', function (err, result, body) {
if (err) {
return void cb(err);
}
request.get('http://some.url/b/' + body.id, function (err, result, body) {
if (err) {
return void cb(err);
}
request.get('http://some.url/c/' + body.id, function (err, result, body) {
if (err) {
return void cb(err);
}
cb(null, result);
});
});
});
}
Asynchronous Functions:
async helpers
function getLatestArticle(userId, cb) {
async.auto({
user: function getUser (cb) {
request.get(`http://some.api/users/${userId}`, cb);
},
latestBlogArticles: ['user', function (cb, {user}) {
const blogTasks = user.blogIds.map(id => function (cb) {
async.waterfall([
function (cb) { request.get(`http://some.api/blogs/${id}`, cb); },
function (blog, cb) {
const latestArticleId = blog.articleIds[blog.articleIds.length - 1];
request.get(`http://some.api/articles/${latestArticleId}`, cb);
},
], cb);
});
async.parallelLimit(blogTasks, cb);
}],
latestArticle: ['latestBlogArticles', function (cb, {latestBlogArticles}) {
const latest = latestBlogArticles.sort((a, b) => a.timestamp - b.timestamp)[0];
cb(null, latest);
}]
}, function (err, {latestArticle}) {
cb(err, latestArticle);
});
}
Asynchronous Functions:
Promises
function getLatestArticle(userId) {
return request.get(`http://some.api/users/${userId}`)
.then(([user]) => {
const latestArticlePromises = user.blogIds.map(id =>
request.get(`http://some.api/blogs/${id}`)
.then(blog => {
const latestArticleId = blog.articleIds[blog.articleIds.length - 1];
return request.get(`http://some.api/articles/${latestArticleId}`);
})
);
return Promise.all(latestArticlePromises);
})
.then(latestBlogArticles => {
return latestBlogArticles.sort((a, b) => a.timestamp - b.timestamp)[0];
});
}
Asynchronous Functions:
ES7 Async Functions!
// Still being standardized!
async function getLatestArticle(userId) {
const user = await request.get(`http://some.api/users/${userId}`);
const latestArticles = await* user.blogIds.map(id =>
request.get(`http://some.api/blogs/${id}`)
.then(blog => {
const latestArticleId = blog.articleIds[blog.articleIds.length - 1];
return request.get(`http://some.api/articles/${latestArticleId}`);
})
);
return latestArticles.sort((a, b) => a.timestamp - b.timestamp)[0];
}
Events:
Callback Properties
webSocket.onopen = function (e) {
};
webSocket.onmessage = function (e) {
};
webSocket.onerror = function (e) {
};
webSocket.onclose = function (e) {
};
Events:
Event Listeners
document
.getElementById('the-button')
.addEventListener('click', function (e) {
// ...
});
Events:
EventEmitter (streams, for example)
stream
.on('data', function (chunk) {
// Handle data
})
.on('error', function (err) {
// Handle error
})
.on('end', function () {
// Handle end
});
Events:
and time-based events
setTimeout(function () {
// ...
}, 1000);
setInterval(function () {
// ...
}, 1000);
requestAnimationFrame(function () {
// ...
});
setImmediate(function () {
// ...
});
And probably more!
These are all isomorphic!
Events: Moments in time + data
Async results = Moment in time + data
A node.js stream:
a sequence of (data) events over time
An EventEmitter:
Emits a sequence of events over time
An EventListener:
Receives a sequence of events over time
A setInterval handler:
Emits a sequence of events (callbacks) over time
A setTimeout handler:
Emits a sequence (limit 1) of events over time
A Promise:
Emits a sequence of (one result or error) events over time
Callbacks:
Emit a sequence of VaLUES over time
Iterables (arrays, generators, etc) are isomorphic!?:
provideS a sequence of VALUES over time (immediately)
Because these are isomorphic, they can all be adapted into one unified type.
Observables,
a.k.a. Streams,
a.k.a. signals
// Subscribe/observe
someStream
.onValue(...)
.onError(...)
.onEnd(...);
// Unsubscribe/unobserve
someStream
.offValue(...)
.offError(...)
.offEnd(...);
Reactive Programming:
Modeling your application as THE PROPAGATION OF CHANGE through DATA FLOWS (a.k.a. event streams!)
With everything adapted into a unified type, you can easily compose them with a single toolkit.
Functional Reactive Programming:
managing DATA flows (event streams) through functional programming techniques
Map
Time
circles.map(convertToTriangles)
flatMap
Time
items.flatMap(i => i.getDerivedStream())
Filter
Time
faces.filter(f => f.isHappy())
Scan
Time
0
7
7
12
19
1
20
-8
12
2
14
-5
9
numbers.scan((sum, n) => sum + n, 0)
Throttle
Time
noisyChannel.throttle(1000)
Debounce
Time
twitchySignal.debounce(200)
Zip
Time
stream.zip(requests, responses)
Merge
Time
stream.merge([likes, shares, retweets])
Combine
Time
stream.combine([profile, ranking, history])
GO2L.INK/frpgame
State Server
Player Agent
Player Agent
Player Agent
Red: 12 Blue: 56
Guidelines:
- All data & events are streams! Stream all the things!
- Make each stream as simple and single-purpose as possible so they can be easily composed.
- Map out your flow of data.
- Data flows must be uni-directional.
- Flows cannot be circular; if an arrow is pointing up, something is probably wrong.
- Something must pull data from a stream; make sure at the very bottom, something is consuming the data, or there will be no data flowing.
RxJs
HIGHLAND.js
Kefir
Flyd
const connections = kefir.repeat(
() => kefir.stream(
emitter => {
setTimeout(() => {
const socket = new WebSocketClient(url);
socket.onopen = () => emitter.emit(socket);
socket.onerror = err => emitter.error(err);
socket.onclose = () => emitter.end();
}, 1000);
}
)
.endOnError()
);
const inboundMessages = connections
.flatMapLatest(s => kefir.stream(
emitter => s.onmessage = emitter.emit))
.map(e => JSON.parse(e.data));
const stateMessages = inboundMessages
.filter(msg => msg.type === 'state')
.map(msg => msg.state);
const timeDrift = inboundMessages
.map(msg => msg.timestamp - Date.now())
.slidingWindow(5, 1)
.map(lags =>
lags.reduce((sum, lag) => sum + lag, 0)
/ lags.length);
const state = kefir
.combine([stateMessages], [timeDrift])
.map(([state, drift]) => {
state.countdown = state.countdown - drift;
return state;
});
const animationFrames = kefir.repeat(
() => kefir.fromCallback(
cb => requestAnimationFrame(cb)
)
);
class PlayerState extends React.Component {
componentDidMount() {
kefir
.combine([animationFrames], [state])
.onValue(([_,s]) => this.setState(s));
}
render() {
return <PlayerInterface {...this.state}/>;
}
}
class PlayerInterface extends React.Component {
// ...
}
const dispatcher = new EventEmitter();
class PlayerInterface extends React.Component {
...
handleJoin(e) {
e.preventDefault();
dispatcher.emit(
'join',
{ name: this.state.name });
}
click(e) {
e.preventDefault();
dispatcher.emit('click', {});
}
}
const clicks = kefir
.fromEvents(dispatcher, 'click')
.map(e => ({ type: 'click' }));
const joins = kefir
.fromEvents(dispatcher, 'join')
.map(e => ({ type: 'join', name: e.name }));
const outboundMessages = kefir.merge([
clicks,
joins
]);
kefir
.combine([outboundMessages], [connections])
.onValue(([msg, socket]) => {
socket.send(JSON.stringify(msg));
});
const roster = dynamicValue([],
joinings, (players, req) => {
const message = req.message;
const name = getUniqueName(message.name, players);
const teamCounts = players.reduce(
(counts, p) => {
counts[p.team]++;
return counts
},
{ Red: 0, Blue: 0 });
const team =
teamCounts.Red <= teamCounts.Blue
? 'Red' : 'Blue';
return players.concat({
client: req.client,
id: message.id,
name: name,
team: team
});
},
leavings, (players, req) =>
players.filter(p => p.id !== req.player),
disconnections, (players, socket) =>
players.filter(p => p.client !== socket)
);
Common Gotchas
Model the problem, then code
Common Gotchas
circular dependencies
const timerSources = kefir.pool();
const completedTimers = timerSources.flatMap(
t => waitUntil(t.timestamp).map(() => t.status));
const canStart = roster.map(r => hasOpponents(r));
const waits = kefir
.merge([
serverStarts,
completedTimers.filter(s => s === 'finishedRound')])
.filterBy(canStart.map(b => !b))
.map(() => ({
status: 'finishedWait',
timestamp: Date.now() + 30000 }));
const countdowns = completedTimers
.filter(s => s === 'finishedWait')
.filterBy(canStart)
.map(() => ({
status: 'finishedCountdown',
timestamp: Date.now() + 5000 }));
const starts = completedTimers
.filter(s => s === 'finishedCountdown')
.map(() => ({
status: 'finishedRound',
timestamp: Date.now() + 5000 }));
timerSources.plug(waits);
timerSources.plug(countdowns);
timerSources.plug(starts);
Common Gotcha:
Lazy Subscriptions
const gameState = inboundStateServerMessages
.filter(m => m.type === 'state')
.map(({ state }) => {
let teamData = {
Red: { playerCount: 0, score: state.teamScores.Red },
Blue: { playerCount: 0, score: state.teamScores.Blue }
};
let playersById = {};
let topPlayer = null;
state.players.forEach(p => {
playersById[p.id] = p;
const team = teamData[p.team];
team.playerCount++;
topPlayer = (topPlayer && topPlayer.score > p.score) ? topPlayer : p;
});
return {
players: playersById,
teams: teamData,
status: state.status,
countdown: state.countdown,
topPlayer: topPlayer
};
})
.onValue(/* Eagerly consume */ () => { })
.toProperty();
Why FRP?
Convert procedural, low-level event handlers to declarative, high-level operations using well-known operators from functional programming.
With FRP, asynchronous code is more elegant, because it is:
- Concise: less boilerplate; common tasks can be performed with a minimum of code.
- Clear: cleanly model your code as data flows transformed using well-understood operations.
- Consistent: merge the hodgepodge of different async patterns into one single data type.
Thank you!
https://github.com/DullReferenceException/braziljs-frp-demo
@apocko
frp
By Jacob Page
frp
Writing elegant JavaScript with Functional Reactive Programming
- 2,591