Describes how to use functional programming techniques with JavaScript, with specific examples around AJAX XHR requests. Presented at Ajax Experience 2008.
You can run the samples and view additional descriptions http://osteele.dev/talks/ajaxian-2008/samples/.
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
Â
Practical Functional Javascript
1. Practical Functional
JavaScript
Oliver Steele
Ajax Experience
Wednesday, 1 October 2008
2. Teasers
⢠AJAX is all about waiting for someone*, and
remembering what you were going to do
when they got back to you.
⢠Functions : interactions :: objects : domains
⢠You didn't really want threads anyway.
(Most of the time.)
* user, web server, other server, wall clock, plugin
3. About Me
implementing using
where? what? graphics languages graphics languages
Ruby bindings for âMinority
Oblong Industries
Reportâ-style interfaces â â
BrowseGoods
Entrepreneurial & Style&Share
Consulting Fansnap â â
Webtop Calendar
Laszlo Systems OpenLaszlo â â
Apple Advanced Dylan â â
Technology Group (programming language)
Apple System Software Skia â â
(graphics library)
4. About You
Raise your hand if you know*:
⢠Closures
⢠Ruby / Smalltalk
⢠XHR / AJAX
⢠An AJAX framework (Prototype / jQuery / âŚ)
⢠Threads
* none of these are required; this just helps me calibrate the talk
5. Agenda
⢠Context ⢠Callbacks
⢠Example Applications ⢠Queueing & Throttling
⢠MVC on the Client ⢠Caching & Joining
⢠Retries & Failover
⢠Fundamentals
⢠Closures (review) ⢠Conclusions
⢠Making Functions
⢠Decorating Functions
⢠Q&A
⢠Some Idioms
6. Non-Agenda
⢠Comet, Bayeux, Gears
⢠Frameworks*
⢠Theory (this isn't your Monad or CPS ďŹx)
⢠Security (standard practices apply)
* This talk should help you understand their implementation and use, but doesn't explore
their APIs in any depth
10. BrowseGoods
Capabilities
⢠Background prefetch of item maps
⢠Background fetch of saved items
⢠Operations on saved items are queued until
initialization is complete
11. Style & Share Server Architecture
Client
Internet
Web Application Processes
Catalog Cart Image
Controller Controller Controller
Catalog
Model
Product Image
Request Catalog Carts Images Request
Queue Queue
Catalog
Image Editor
Updater
Cart Sweeper
Image
ECS Client
Retriever
Catalog Update Process Garbage Collector Image Process
Client Architecture
Internet
Catalog
Cart Manager
Search Gallery
ECS Cart Server Cart Selector
ECS Search Amazon ECS Amazon Image
Proxy Proxy Server Servers
ECS Connection Server Proxy
AJAX Throttle
Task Scheduler
Internet
ECS Server Style&Share Server
12.
13. Style & Share
Capabilities
⢠Retry with exponential backoff and failover
⢠Explicit queues to control serialization
order
⢠Background prefetch for catalog items
⢠Multiple queues to prioritize user
interaction
14. Fansnap
⢠Visualize available seats
in a venue
⢠Seatmap is OpenLaszlo
(compiled to Flash)
⢠Widgets are in jQuery
15.
16. FanSnap Capabilities
(Seatmap)
⢠Two-way asynchronous communication
between the Flash plugin and the HTML
⢠Asynchronous communication between the
Flash plugin and the server
⢠Again, initialization is particularly challenging
17. Webtop Calendar
Webtop Calendar Client Data Architecture
Calendar Client
View Layer
Event
Event Model Calendar Model
Observer
Data Layer
Event Calendar Calendar Service
Cache Collection
Range Event Report
Cache Cache
Serializer CalendarConnection
Webtop Client Library
Internet
Webtop Server
18.
19. Webtop Calendar
Capabilities (Data Model)
⢠Synchronizes local model with server
model
⢠Local model is cache: some operations
update it; others invalidate it
⢠Race conditions, where prefetch overlaps
operations that invalidate the cache
20. Motivating Problem:
Web MVC
Server Client
View View
Model Model User
Controller Controller
27. What is a Function? Create
Call graph
⢠Math: rule that maps inputs to outputs
Invoke
Docs
⢠Computer science: abstracted computation
Specs with effects and outputs
⢠Source Runtime
Software engineering: one of several units
Register
forCode Object
documentation, testing, assertions, and
Defân
analysis
⢠Source code: unit of source text with Store
inputs and outputs
Contract
Implementation
⢠Runtime: invocable parameterized behavior
Pass
28. Nested Functions:
Globals
function callback(x) {
log('received quot;' + x + 'quot;');
}
Â
function request() {
$.get('/request', callback);
}
Â
request();
Â
Â
29. Nested Functions:
Variables
var callback = function(x) {
log('received quot;' + x + 'quot;');
}
Â
var request = function() {
$.get('/request', callback);
}
Â
request();
Â
Â
30. Nested Functions:
Function-First Style
function request() {
function callback(x) {
log('received quot;' + x + 'quot;');
}
$.get('/request', callback);
}
Â
request();
Â
Â
31. Nested Functions:
Pyramid Order
function request() {
$.get('/request', callback);
function callback(x) {
log('received quot;' + x + 'quot;');
}
}
Â
request();
Â
Â
32. Nested Functions:
Inlining the Function
function request() {
$.get('/request', function callback(x) {
log('received quot;' + x + 'quot;');
});
}
Â
request();
Â
Â
33. Nested Functions:
Anonymous
function request() {
$.get('/request', function (x) {
log('received quot;' + x + 'quot;');
});
}
Â
request();
Â
34. Creating Functions:
Basics
function makeConst1() {
return function() { return 1; }
}
Â
function const1a() { return 1; }
var const1b = function() { return 1; }
var const1c = makeConst1();
Â
log(const1a());
log(const1b());
log(const1c());
Â
log(makeConst1()());
Â
Â
35. Creating Functions:
Variable Capture
function makeConstN(n) {
return function() { return n; }
}
Â
var const10 = makeConstN(10);
log(const10());
log(const10(100));
Â
var const20 = makeConstN(20);
log(const20());
log(const20(100));
Â
Â
36. Creating Functions:
Capturing More Variables
function makePlus1() {
return function(x) { return x + 1; }
}
log(makePlus1()(10));
function makePlusN(n) {
return function(x) { return x + n; }
}
var plus10 = makePlusN(10);
log(plus10(100));
Â
Â
Â
37. Creating Functions:
Function-Valued Parameters
function plus1(x) {
return x+1;
}
Â
function twice(fn) {
return function(x) {
return fn(fn(x));
}
}
Â
var plus2 = twice(plus1);
log(plus2(10));
Â
Â
38. Creating Functions:
Storage
var FnTable = {
'+1': function(n) { return n+1; },
'+2': function(n) { return n+2; }
};
Â
log(FnTable['+1'](10));
log(FnTable['+2'](10));
Â
Â
39. Creating Functions:
Building a Registry
var FnTable = {};
function register(name, fn) { FnTable[name] = fn; }
function tableMethod(name) { return FnTable[name]; }
function makeAdder(n) {
return function(x) { return x + n; }
}
Â
register('+1', makeAdder(1));
register('+2', makeAdder(2));
log(tableMethod('+1')(10));
log(tableMethod('+2')(10));
Â
Â
40. Creating Functions:
Manual Guards
for (var i = -5; i < 5; i++)
log(i);
Â
Â
function callIfPositive(fn) {
return function(x) {
return x > 0 ? fn(x) : undefined;
}
}
Â
var logIfPositive = callIfPositive(log);
Â
for (var i = -5; i < 5; i++)
logIfPositive(i);
Â
Â
41. Creating Functions:
Guard Construction
function guard(fn, g) {
return function(x) {
return g(x) ? fn(x) : undefined;
}
}
Â
function callIfPositive(fn) {
return guard(fn, function(x) { return x > 0; });
}
Â
var logIfPositive = callIfPositive(log);
Â
for (var i = -5; i < 5; i++)
logIfPositive(i);
Â
42. Closures (1)
var get, set;
function assignAccessors() {
var x = 1;
get = function() { return x; }
set = function(y) { x = y; }
}
assignAccessors();
log(get());
set(10);
log(get());
43. Closures (2)
function makeAccessors() {
var x;
return {get: function() { return x; },
set: function(y) { x = y; }}
}
var gf1 = makeAccessors();
var gf2 = makeAccessors();
gf1.set(10);
gf2.set(20);
log(gf1.get());
log(gf2.get());
Â
44. Function Construction Idioms:
Special Variables
// 'this' and 'arguments' are special
function f() {
logArguments(this, arguments);
}
f();
f('a');
f('a', 'b');
Â
Â
45. Function Construction Idioms:
Function Call and Apply
function f() { logArguments(this, arguments); }
// these are equivalent:
f(1, 2, 3);
f.call(window, 1, 2, 3);
f.apply(window, [1, 2, 3]);
Â
Â
46. Function Construction Idioms:
Method Call and Apply
var obj = {
f: function() {
logArguments(this, arguments); }};
// and so are these:
obj.f(1, 2, 3);
obj.f.call(obj, 1, 2, 3);
obj.f.apply(obj, [1, 2, 3]);
47. Function Construction Idioms:
Borrowing Methods (Bad)
// stealing a method the wrong way
var o1 = {name: 'o1',
show: function() { log(this.name); }};
var o2 = {name: 'o2'};
o1.show();
o2.show = o1.show;
o2.show();
delete o2.show;
Â
Â
48. Function Construction Idioms:
Borrowing Methods (Better)
// using 'apply' to steal a method
var o1 = {name: 'o1',
show: function() { log(this.name); }};
var o2 = {name: 'o2'};
o1.show();
o1.show.call(o2);
o1.show.apply(o2, []);
Â
49. Function Construction Idioms:
arguments is special
// arguments isn't an Array, so this doesn't work:
function capture() {
var args = arguments.slice(0);
// ...
}
Â
// instead, steal the 'slice' method from an instance of Array,
// and apply it:
function capture() {
var args = [].slice.call(arguments, 0);
// ...
}
Â
// or just take it from Array's prototype
function capture() {
var args = Array.prototype.slice.call(arguments, 0);
// ...
}Â
50. Function Construction Idioms:
Wrapping Variadic Functions
function id1(fn) {
return function(x) {
return fn(x);
}
}
Â
function id2(fn) {
return function(x, y) {
return fn(x, y);
}
}
Â
function idn(fn) {
return function() {
return fn.apply(this, arguments);
}
}
51. Function Construction Idioms:
Record and Replay
var queue = [];
function capture() {
queue.push(Array.prototype.slice.call(arguments, 0));
}
function replay() {
while (queue.length)
fn.apply(null, queue.shift());
}
function fn(a, b) {
log(a + ' + ' + b + ' = ' + (a+b));
}
capture(1,2);
capture(1,3);
capture(10,20);
replay();
52. Function Construction Idioms:
Extending Functionâs Prototype
Function.prototype.twice = function() {
var fn = this;
return function() {
return fn.call(this, fn.apply(this, arguments));
};
}
function plus1(x) { return x+1; }
var plus2 = plus1.twice();
log(plus2(10));
Â
53. Summary
⢠Functions are values
⢠Functions can be arguments, return
values, array elements, property values
⢠Functions can be created and âmodiďŹedâ
⢠Argument lists can be saved, modiďŹed, and
replayed
54. Case Study: Callbacks
Server Client
<%@page
<html>
<?= t
<meta
<canvas
<text
<view
<?xml v
<%@ tag
<xslt:t
XML <?xml
<root>
<row>
</row>
<?xml
<root>
<row>
</row>
<?xml v
<%@ tag
XML
<xslt:t
<?xml
<root>
<row>
</row>
56. Throttling:
Unthrottled
for (var i = 0; i < 10; i++)
$.get('/services/time', log);
Â
Â
57. Frequency Throttling:
Call Site
var gCounter = 0;
function runNext() {
if (gCounter < 10) {
setTimeout(function() {
$.get('/services/time', log);
runNext();
}, 1000);
gCounter++;
}
}
runNext();
Â
Â
58. Frequency Throttling:
Manual Wrapper
var gQueue = [];
var gNextTime = 0;
$.throttled = function(url, k) {
gQueue.push([url, k]);
if (gQueue.length == 1)
schedule();
function schedule() {
setTimeout(function() {
gNextTime = new Date().getTime() + 1000;
var entry = gQueue.shift();
$.get(entry[0], entry[1]);
if (gQueue.length)
schedule();
}, Math.max(0, gNextTime - new Date().getTime()));
}
};Â
for (var i = 0; i < 10; i++)
Â
$.throttled('/services/time', log);
59. Frequency Throttling:
Function Constructor
function makeThrottled(fn, interval) {
var queue = [];
var nextTime = 0;
return function() {
queue.push(Array.prototype.slice.call(arguments, 0));
if (queue.length == 1) schedule();
}
function schedule() {
setTimeout(function() {
nextTime = new Date().getTime() + interval;
fn.apply(null, queue.shift());
if (queue.length) schedule();
}, Math.max(0, nextTime - new Date().getTime()));
}
}
$.throttled = makeThrottled($.get, 1000);
for (var i = 0; i < 10; i++)
$.throttled('/services/time', log);
Â
Â
60. Count Throttling:
Manual Wrapper
var gQueue = [];
var gOutstanding = 0;
$.throttled = function(url, k) {
function k2() {
gOutstanding--;
k.apply(this, arguments);
if (gOutstanding < 2 && gQueue.length) {
var entry = gQueue.shift();
$.get(entry[0], entry[1]);
}
}
if (gOutstanding < 2) {
gOutstanding++;
$.get(url, k2);
} else
gQueue.push([url, k2]);
};
for (var i = 0; i < 10; i++)
$.throttled('/services/sleep/2', log);
Â
Â
61. Count Throttling:
Constructor
function makeLimited(fn, count) {
var queue = [];
var outstanding = 0;
return function() {
var args = Array.prototype.slice.call(arguments, 0);
// replace the last arg by one that runs the
// next queued fn
args.push(adviseAfter(args.pop(), next));
if (outstanding < count) {
outstanding++;
fn.apply(this, args);
} else
queue.push(args);
}
function next() {
if (queue.length)
fn.apply(null, queue.shift());
}
}
$.throttled = makeLimited($.get, 2); for (var i = 0; i < 10; i++)
$.throttled('/services/sleep/2', log);
62. Frequency Throttling:
Function Constructor
function makeThrottled(fn, interval) {
var queue = [];
var nextTime = 0;
return function() {
queue.push(Array.prototype.slice.call(arguments, 0));
if (queue.length == 1) schedule();
}
function schedule() {
setTimeout(function() {
nextTime = new Date().getTime() + interval;
fn.apply(null, queue.shift());
if (queue.length) schedule();
}, Math.max(0, nextTime - new Date().getTime()));
}
}
$.throttled = makeThrottled($.get, 1000);
for (var i = 0; i < 10; i++)
$.throttled('/services/time', log);
Â
Â
63. Retry
$.getWithRetry = function(url, k) {
var countdown = 10;
$.ajax({url:url, success:k, error:retry});
function retry() {
if (--countdown >= 0) {
log('retry');
$.ajax({url:url, success:k, error:retry});
}
}
};
$.getWithRetry('/services/error', log);
Â
Â
64. Throttled Retry
var gPageLoadTime = new Date;
$.getWithRetry = function(url, k) {
var countdown = 10;
var delay = 1000;
var nextTime = new Date().getTime() + delay;
$.ajax({url:url, success:k, error:retry});
function retry() {
if (--countdown >= 0) {
setTimeout(function() {
delay *= 1.5;
log('retry@t+' + (new Date - gPageLoadTime)/1000 + 's');
nextTime = new Date().getTime() + delay;
$.ajax({url:url, success:k, error:retry});
}, Math.max(0, nextTime - new Date().getTime()));
}
}
};
$.getWithRetry('/services/error', log);
65. Failover
$.getWithFailover = function(urls, k) {
$.ajax({url:urls.shift(), success:k, error:retry});
function retry() {
if (urls.length)
$.ajax({url:urls.shift(),
success:k,
error:retry});
}
};
$.getWithFailover(
['/services/error', '/services/error', '/services/time'],
log);
Â
66. Caching (1)
var gRequestCache = {};
$.cachedGet = function(url, k) {
if (url in gRequestCache)
k(gRequestCache[url], 'success');
else
$.get(url, adviseBefore(k, function(result, status) {
if (status == 'success')
gRequestCache[url] = result;
}));
};
$.cachedGet('/services/random', log);
$.cachedGet('/services/random', log);
$.cachedGet('/services/echo/1', log);
$.cachedGet('/services/echo/2', log);
setTimeout(function() {$.cachedGet('/services/random', log);},
1000);
Â
Â
67. Caching (2)
var gPendingRequests = {};
var gRequestCache = {}; $.cachedGet('/services/random', log);
$.cachedGet = function(url, k) { $.cachedGet('/services/random', log);
if (url in gRequestCache) $.cachedGet('/services/echo/1', log);
$.cachedGet('/services/echo/2', log);
k(gRequestCache[url], 'success');
$.cachedGet('/services/random', log);
else if (url in gPendingRequests)
gPendingRequests[url].push(k);
else {
var queue = [k];
gPendingRequests[url] = queue;
$.get(url, function(result, status) {
if (status == 'success')
gRequestCache[url] = result;
while (queue.length)
queue.shift().call(this, result, status);
delete gPendingRequests[url];
});
}
};Â
68. Caching (3)
function memoizedContinuation(fn) {
$.cachedGet('/services/random', log);
var cache = {};
return function(key, k) { $.cachedGet('/services/random', log);
if (key in cache) $.cachedGet('/services/echo/1', log);
k(cache[key]); $.cachedGet('/services/echo/2', log);
else $.cachedGet('/services/random', log);
fn(key, k); Â
}
}
function consolidateContinuations(fn) {
var queues = {};
return function(key, k) {
if (key in queues)
queues[key].push(k);
else {
var queue = queues[key] = [k];
fn(key, function(value) {
while (queue.length)
queue.shift().call(this, value);
delete queues[key];
});
}
}
}
$.cachedGet = consolidateContinuations(memoizedContinuation($.get));
69. Summary
⢠Functions-as-objects allow separation of
concerns
⢠Factor how, when, and whether from what
⢠Functions are to interaction patterns as objects
are to domains