Asynchronous Javascript
Parallel vs Async
Kyle uses the likeness of a rollercoaster to describe non-parallel. The example states that there is a line `for a rollercoaster which can hold 30 people at once. When you get to the front, although it can take 30 people, you are the only person who goes on the ride. This contrasts with parallel, in which 30 people would all be on the ride with you at the same time. Parallelism is most commonly expressed with threads in computing. Parallelism is all about optimisation.
Asynchronicity moves us to just a single thread. Our program only runs on a single thread, with only one line ever running at one time in the JS engine. This brings us to concurrency. This could be described as two higher level tasks, happening within the same time frame. Like how a CPU operates, events can be interleaved, to give the appearance of running at the same time/concurrently, whereas in reality, they are simply taking turns.
Callbacks
setTimeout(function () {
console.log('callback')
}, 1000)
Continuations are == to callbacks. Above is a simple example of using a timeout to make our function wait, before completing execution. 'Callback hell' typically refers to multiple nested callbacks, each with their own timeouts.
Inversion of Control
trackCheckout(purchaseInfo, function finish() {
chargeCreditCard(purchaseInfo)
showThankYouPage()
})
Using this as a real-world example, we may come across some scenario where a customers credit card is charged more than once for the same transaction, even if this is the only time we make a call to chargeCreditCard. This may be because chargeCreditCard is some API which we make a call to, and we are surrendering our control to them. Perhaps they had some experimental code which invoked our callback function multiple times if no response was received.
When we pass a callback, we trust that it:
- It won't call too early
- Or call too late
- Too Many times
- It will call enough times
- No context is lost
- No errors are swallowed
Callbacks are the only way that we can express a temporal dependency. We also want to be able to make our synchronous code 'more reasonable' in that they make logical sense.
Thunks
From a synchronous perspective, a thunk is a function which already has everything it needs to be able to give you a value back.
function add(x, y) {
return x + y
}
var thunk = function () {
return add(10, 15)
}
thunk()
An asynchronous thunk is a function which doesn't need any arguments passed to it to do it's job, except it requires a callback to be able to return a value.
function addAsync(x, y, cb) {
setTimeout(function () {
cb(x + y)
}, 1000)
}
var thunk = function (cb) {
addAsync(10, 15, cb)
}
thunk(function (sum) {
sum
})
Promises
Promises are required as a placeholder, to eliminate time as a concern which is wrapped around a value. Kyle uses the example of ordering a burger, with the receipt being the promise, and when your order is ready/called, you exchange the promise (receipt) for the value which was promised by the receipt (the burger). A promise is like an event listener.
function finish() {
chargeCreditCard(purchaseInfo)
showThankYouPage()
}
function error(err) {
logStatsError(err)
finish()
}
var promise = trackCheckout(purchaseInfo)
promise.then(finish, error)
There are still callbacks in this solution, so we may be asking how this version is better than prior ones?
Promises are designed to instil trust in us.
Promise Trust:
- Only resolved once
- Either success or error
- Messages are either passed or kept
- Exceptions become errors
- Immutable once resolved
Promises could almost be thought of as a callback manager. Managing them in a trusted state.
Promise Flow Control
We can chain promises together, that are called sequentially once the previous promise has resolved. If at any point we receive an error, we want to stop execution and throw.
doFirstThing()
.then(function () {
return doSecondThing()
})
.then(function () {
return doThirdThing()
})
.then(complete, error)
Do first thing gives us a promise, we set up a then, and complete whatever we need, then return doSecondThing. By returning it, it means that the next .then handler now waits for the promise returned from 'return doSecondThing()', instead of immediately executing.
Abstractions
- Promise.all: Takes an array of promises, and makes us wait until all promises are completed. The following .then() will take a function with an array as the parameter.
- Promise.race: Also takes an iterable of promises, however the returned promises settles with the value of the promise which first settles (and only that one)
- Promise.any: Takes an iterable of promises, returns a single promise. Returned promise is fulfilled when any input's promise fulfils. Only rejects if all input promises reject.
Generators (yield)
Generators are about solving the non-local, non-sequential reasonability problem. When function execute, the entire function (run to completion semantic) is run before any other function can be executed. Obviously functions can still call other functions. Generators do not have this run to completion semantic.
Yield in a generator is similar to a pause button. We can stop when we hit a yield keyword, then come back and complete execution later. This means generators are essentially a pauseable function. This blocking/pausing is only local, the rest of the program can still run.
function* gen() {
console.log('Hello')
yield
console.log('World')
}
var it = gen()
it.next() // Hello
it.next() // World
Note the * used to dictate that this function is a generator. Executing a generator doesn't run it's code, instead an iterator is produced. Calling a generator produces an iterator. This is why we use .next() to continue through the generator step by step.
You can also yield values. In this case, .next() will yield an object with the value, and a 'done' boolean which dictates if the generator has completed.
function* main() {
yield 1
yield 2
yield 3
}
var it = main()
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: false }
it.next() // { value: undefined, done: true }
The last .next() call returns undefined, as all functions without a return statement return undefined. This shows that messages can be passed/yielded out. We can also pass messages into the generator. Passing in a value into the generator allows that value to be used. For example:
var run = coroutine(function* () {
// Where coroutine is a function calling .next() with arguments
var x = 1 + (yield)
var y = 1 + (yield)
yield x + y
})
run()
run(10)
console.log('Meaning of life: ' + run(30).value)
- The first run() goes until the first yield
- Run(10) gives the value of 10 to the first yield statement, so now var x can be computed
- Run (30) gives the value of 30 to the second yield statement, so now var y can be computed
Promises and Generators
Using promises and generators together, it will solve our issues with inversion of control and our non-sequential problems. To use them together, we will yield out promises, wait for them to resolve, then go back to the generator to resume execution, and repeat this as required.
An asynquence version of this:
function getData(d) {
return ASQ(function (done) {
setTimeout(function () {
done(d)
}, 1000)
})
}
ASQ()
.runner(function* () {
var x = 1 + (yield getData(10))
var y = 1 + (yield getData(30))
var answer = yield getData('Meaning of life: ' + (x + y))
})
.val(function (answer) {
console.log(answer)
})
Observables
An observable is an adaptor, hooked onto an event source which produces a new promise every time a new event comes through.
// From RsJs
var obsv = Rx.Observable.fromEvent(btn, 'click')
obsv
.map(function mapper(evt) {
return evt.target.className
})
.filter(function filterer(className) {
return /foobar/.test(className)
})
.distinctUntilChanged() // Let a piece of data comes through, but not any duplicates, only different ones
.subscribe(function (data) {
var className = data[1]
console.log(className)
})
Reactive Sequences
An example of a reactive sequence:
var rsq1 = ASQ.react.of()
var rsq2 = ASQ.react.of(1, 2, 3)
var x = 10
setInterval(function () {
rsq1.push(x++)
rsq2.push(x++)
}, 500)
rsq1.val(function (v) {
console.log('1:', v)
}) // 1:10 1:12 1:14
rsq1.val(function (v) {
console.log('2:', v)
}) // 2:1 2:2 2:3 2:11 2:13
Here we are essentially pumping data into a stream, and getting some output. The point is to model data flow with stream operations.
CSP
CSP: Communicating sequential processes. CSP is about modelling concurrency with channels. A channel is similar to a stream/pipe, except there is no buffer size. Backpressure exists. Imagine a hose being sprayed, until the user stops the hose with the handle, but there is still backpressure of water in the hose which goes all the way back to the tap (source). Because channels have backpressure, there is no queue, the user can only receive a message (water) when they are ready, otherwise nothing will happen.
CSP could be considered as two generators who block out and wait for each other to be ready before continuing execution. They work together and collaborate.
var ch = chan()
function* process1() {
yield put(ch, 'Hello') // Put() places something into the channel
var msg = yield take(ch) // Take() retrieves something from the channel
console.log(msg)
}
function* process2() {
var greeting = yield take(ch)
yield put(ch, greeting + 'World')
console.log('Done!')
}
// Hello World
// Done
We use generators here with channels, so that generators wait (using yield) until the channel gives them what they have asked from (in this case from another generator).
Blocking Channels
In the below example, csp is used (taken from a library). It is an example of how we could block channels using the library.
csp.go(function* () {
while (true) {
yield csp.put(ch, Math.random())
}
})
csp.go(function* () {
while (true) {
yield csp.take(csp.timeout(500))
var num = yield csp.take(ch)
console.log(num)
}
})
In this example, whilst we are using put to place a random number into the stream, they wish to do this as fast as possible, however the csp.take will only take a value every 500 milliseconds. And due to the structure, this means a value can only be put into the channel every 500 milliseconds, rather than constantly as it wishes. Kyle has also imported csp into Asynquence.