Introduction
Continuation passing style (CPS) is a pattern where the caller of a function pass, as an argument, the continuation function that should be called after the called function has finished. This is very similar to promises, asyn calls, or in general callbacks.
This is sometimes useful as the function can be a blocking call (e.g. a network request) and the program should continue to do useful work while the operation is taking place. To solve that problem, we can call the said function in a separate thread (usually provided as a primitive by the langauge) and provide a callback which will be executed when the function in the separate thread has finished the blocking call. Meanwhile, the main thread can continue to run and do meaningful work.
Transactions
When we have a distributed systems and share data, we can run into consistency problems. Consider the canonical Read After Write (RAW) situation: we have two processes (p
and q
), both are trying to update the value of x
from 1
to 2
and 3
respectively, and both will try to read the value afterwards. Naively, p
can write between q
’s write and read. This results in q
reading 2
when it tried to read 3
Sometimes, it is also useful when we want to mutate our datastore in a consistent manner.
Callback madness
type A struct {
B
C
}
func (a A) foo() {
postNetworkWork := func() error {
return a.B.bar()
}
a.C.doIt(postNetworkWork)
}
type B struct {
// Complex setup required
}
func (b B) bar() error {
// some network logic
}
type C struct {
// Complex setup required
}
func (c C) doIt(postNetworkWork func() error) {
// some mutation logic
if err := postNetworkWork(); err != nil {
// undo mutation
}
}
Ideally, to test this, one might mock out B and C, which A depends on, and test if check if C.doIt
and B.bar
are called. The problem here is that B.bar
is only called if C.doIt
is not a mock. Setting up C
is complex and is not the intention of the test.
This can be too much for unit testing A because A should really be testing whether C.doIt
is called and mocking just C.doIt
might suffice.
However, the problem will return when one wants to do an integration test and calling B.bar is necessary.
Two simple ways to get around this problem are:
- Create a fake C that actually calls a function you define. The tradeoff here is maintaining a sensible logic for the fake when the logic of the real one changes.
- Run with the real B and C with some fake backend. The tradeoff here is a more heavyweight testing environment.