This article is a part of my series about concurrency and asynchronous programming in Swift. The articles are independent, but after reading this one you might want to check out the rest of the series:
The saying holds that the world is supported by a chain of increasingly large turtles. Beneath each turtle is yet another: it is “turtles all the way down”.
In a few episodes of great podcast Analog(ue) hosts Casey and Myke discuss Crash Course Computer Science series and how it presents abstractions. In software engineering and computer science, we constantly deal with layers of abstractions, but we don’t cross too many of them on daily basis. We work with software libraries that are built on top of a few modules and functions, which are written in a programming language, which is executed as binary code, which runs on CPUs and GPUs, which use logic gates built with transistors, to understand which you might need to understand chemistry and physics. Quite possibly someday particle physicists will discover a few more turtles further down the stack. But as long as you use that software library, you don’t need to care about logic gates, transistors and quantum tunnelling.
So here’s an abstraction that developers work with a lot: asynchronous computations. These days it’s hard to write code that doesn’t touch something asynchronous: any kind of I/O, most frequently networking, or maybe long-running tasks that are scheduled on different threads/processes. For something more concrete, consider a database query, which could be local disk I/O or something that sits somewhere on the network.
I want to write better code and build more abstractions where needed and where it makes the most sense. Because of that, I’ve recently spent a lot of time researching and trying to understand different ways to deal with asynchronous code. In a lot of programming languages callbacks is one of the ways to write it, but it’s also a relatively low level to deal with this abstraction. Nevertheless, it’s important to understand how it works on this level and what can be built on top of it. I hope my explanation could be useful to you as well.
In Swift using callbacks is the main way to deal with asynchronicity. While Swift is taken as an example, I don’t use or discusss any advanced features here and I hope it’s going to be interesting even if you don’t use Swift on daily basis.
To pass a callback you could use a “closure”. The Swift Programming Language Guide introduces it this way:
Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.
Closures can capture and store references to any constants and variables from the context in which they are defined. This is known as closing over those constants and variables.
[…]
Global and nested functions, as introduced in Functions, are actually special cases of closures. Closures take one of three forms:
- Global functions are closures that have a name and do not capture any values.
- Nested functions are closures that have a name and can capture values from their enclosing function.
- Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.
Copyright © 2018 Apple Inc. Excerpt provided for personal use.
Here’s a simple example of a closure that returns a sum of captured variable and its own argument.
func example() {
var a = 10
func f(_ x: Int) -> Int {
return x + a
}
f(5)
// x = 5, a = 10, returns 15
a = 15
f(5)
// x = 5
// a in captured environment is 15
// f returns 20
let g = f
// assigned closure f to a new constant g
g(5)
// x = 5
// g now references same environment,
// where a is set to 15
// g returns 20
a = 20
f(5)
// a in captured environment is 20
// f returns 25
g(5)
// g shares environment with f,
// where a is set to 20
// g returns 25
}
At this point, we can say that a closure is just function code with some captured environment. “Environment” means variables and constants defined outside of closure body. “Capturing” means you have access to a variable value in the environment. If that variable was subsequently modified before closure execution, code within the closure would get the latest up-to-date value. Obviously, constant values are captured as well, but by definition, they can’t be changed.
The example code is wrapped in a function to demonstrate that environment can be
local to a current scope as opposed to global environment. If variable a
was
declared globally, there wouldn’t be anything interesting in using its value,
but capturing access to it in local scope requires special treatment in the
language implementation.
As seen in the example above, closures have reference semantics, not value semantics. Assigning a closure to a variable doesn’t create a copy of that closure and its environment, but only creates a reference to that. If you were to pass a closure as an argument, it would be passed by reference, not by value.
A more interesting example of closure and callback use:
let backend = Backend()
func example() {
let indicator = UIActivityIndicatorView()
let label = UILabel()
// non-blocking code
backend.login(email, password,
onCompletion: { success, failure in
// code called when `backend.login` completes.
// indicator variable captured here
indicator.stopAnimating()
// UILabel instance is captured here
label.text = "\(success ?? failure)"
})
// ...
// some other code here that
// won't be blocked by `backend.login`
// e.g. UI updates
indicator.startAnimating()
}
Because explaining closures in terms of closures doesn’t make much sense, let’s imagine a programming language that doesn’t have them. To tell a story about the closure turtle, we need to place it on top of other turtles.
What follows is an example of how this could work. It doesn’t require any knowledge of how compilers work, it’s just a lower level representation of closures for our studies.
Consider a subset of Swift, where functions (including anonymous functions) can’t capture the outside environment. That is a language where this:
func example() {
var a = 5
func f(_ x: Int) -> Int {
return x + a
}
let g = { x in return x + a }
}
would cause two compiler errors about a
being undefined in the body of functions f
and g
.
To implement closures in this imaginary language, we’d need to use classes to get the reference semantic mentioned above. It’s also useful to have a protocol defining a generic closure shape. It could look like this:
protocol Closure: class {
associatedtype Environment
associatedtype Input
associatedtype Output
var env: Environment { get set }
func willRun(_: Input) -> Output
}
And here’s a class conforming to this protocol and implementing our closure with example usage code:
class F: Closure {
class Environment {
var a: Int
init(_ a: Int) {
self.a = a
}
}
var env: Environment
init(_ e: Environment) {
self.e = e
}
func willRun(_ x: Int) -> Int {
return x + e.a
}
}
func example() {
var e = F.Environment(10)
let f = F(e)
f.willRun(5)
// returns 15
e.a = 15
f.willRun(5)
// returns 20
let g = f
g.willRun(5)
// returns 20
e.a = 20
f.willRun(5)
// returns 25
g.willRun(5)
// returns 25
}
Well, this is verbose. But it’s a pattern you might consider when writing in C,
C++ before C++11 and its support for lambdas, or Objective-C before blocks were
introduced. And here’s a point that I often don’t see mentioned when closures
are explained: you can think of closures as syntactic sugar for anonymous
delegates. If you have
experience with Objective-C or Cocoa, you might have noticed the
analogy
already, I deliberately named the protocol function willRun
similarly to Cocoa
delegate function naming pattern.
Under the hood, a closure-capable compiler will generate a separate function
body with a corresponding environment when it sees there are actual variable
captures going on. In some implementations, like C++11, you have to specify a
list of captured variables explicitly. In Objective-C, that’s what the __block
modifier is for, while in Swift it’s automatic by default with a possibility to
override it. The latter is quite handy when you want some variables to be
captured with weak references to avoid reference cycles.
In a lot of cases using the right abstractions saves us from verbosity in our code. Are callbacks a good abstraction for all asynchronous scenarios then?
Consider a more complex version of the code we’ve seen before:
let label = UILabel()
// non-blocking code
backend.login(email, password,
onCompletion: { success, failure in
// called when `backend.login` completes.
// UILabel instance is captured here
label.text = success
guard !failure else {
// ...
// error handling here
return
}
// notice `backend` is captured here as well
backend.fetchUserInfo { userInfo, failure in
guard !failure else {
// ...
// error handling here again
return
}
if userInfo.isAdmin {
backend.doAdminStuff { success, failure in
guard !failure {
// tired of repeated manual
// error handling by this point...
return
}
// ...
// more code here to
// handle admin stuff results
}
}
}
})
// ...
// some other code here that
// won't be blocked by `backend.login`
To handle a sequence of async actions we need to use nested callbacks. Turns out
it’s quite easy to start building these pyramids of doom. Error handling is
quite tedious and overall this type of code becomes quite error-prone with
shadowed variables all around: notice shadowed failure
callback argument
within all completion handlers. With plain callbacks, this isn’t an exaggerated
scenario, you can often stumble upon this when building real apps that involve
any kind of long-running non-blocking operation, e.g. networking.
There is a way out, consider a concept called “futures”, also know as “promises”. Using BrightFutures library as an example, the pyramid of doom can be converted to this flat chain of futures:
let label = UILabel()
// non-blocking code
backend.login(email, password)
.flatMap { success in
// called when `backend.login` succeeds.
// UILabel instance is captured here
label.text = success
return backend.fetchUserInfo()
}
.onSuccess { userInfo in
if userInfo.isAdmin {
return backend.doAdminStuff()
.onSuccess {
// ...
// more code here to handle
// admin stuff results
}
}
}
.onFailure {
guard !failure else {
// ...
// unified error handling here,
// that can be triggered by any
// future in the chain
return
}
}
// ...
// some other code here that
// won't be blocked by `backend.login`
That’s much better, we have only one error handler that will be triggered
regardless of where an error might occur. There is only one nested callback
after backend.fetchUserInfo()
and only because we have other async code
executed on a condition that our user is an admin. More readability and much
easier to maintain and refactor if you need to re-order the async operations.
This code assumes you’ve converted all callback-based functions like login
,
fetchUserInfo
and doAdminStuff
to return futures. What’s a future? Think of
it as a container that can only be opened at some later time (in the future) and
it might contain either an actual result or an error. You can attach callbacks
to a future “container” to be triggered when the computation is ready, and you
can chain these containers together to run a sequence (or sometimes a parallel
batch) of asynchronous computations. A simplified declaration could look like
this:
class Future<Result> {
func onSuccess(callback: (Result) -> ())
-> Future<Result>
func onFailure(callback: (Error) -> ())
-> Future<Result>
func map<NewResult>(transformer: (Result) -> NewResult)
-> Future<NewResult>
func flatMap<NewResult>(transformer: (Result) -> Future<NewResult>)
-> Future<NewResult>
}
There’s a peculiar flatMap
function here. In a few other Swift libraries
implementing futures, it’s called then
, which is a more straightforward name
if you don’t focus on the container aspect of futures. In the JavaScript
standard library the Promise
class uses the name
then
as well.
So why name it flatMap
? Turns out there is already a
flatMap
function in Swift standard library defined on the
Sequence
protocol,
which is similar to
map
you’ve probably used if you’re into functional programming. Standard map
transforms a container of elements of type A
to a container of elements of
type B
with a closure of type (A) -> B
. flatMap
does the same for closures
of type (A) -> [B]
, if we assume this container is an array. With simple map
the end result would be of type [[B]]
, which is a container of containers.
This isn’t something you’d always want, so to “flatten” these containers to only
one level [B]
use flatMap
. It works the same way for all types of containers
that implement the Sequence
protocol.
To chain Future<A>
with Future<B>
that depends on result A
, our result
transformer closure would need to look like A -> Future<B>
. Taking this
analogy further, if you applied this closure to simple map
, you’d get the end
result of type Future<Future<B>>
, which is bananas and doesn’t make any sense.
Hence we use flatMap
to get a “flat” result type.
We’ve just reviewed closures and had a quick look at how they’re used for callbacks. We tried to write asynchronous code with callbacks and found that it’s not very maintanable. In the end we tried using futures, which leveraged closures as well, but allowed us to structure the example code in a better way.
Admittedly, this futures stuff is an ok abstraction, but it doesn’t feel as smooth
writing plain old synchronous code. If only we could write non-blocking
asynchronous code that almost looked like usual synchronous code… Turns out,
plenty of programming languages provide a few abstractions to work with
asynchronous code and concurrency, like coroutines and
async/await
syntax sugar in JavaScript,
Python, C# to name a few. Some take a different approach, e.g. Erlang/Elixir and
Pony are built on actor model,
while Go introduces goroutines and
channels.
Special thanks to Neil Kimmett for feedback on draft versions of this article. Thanks to Thomas Visser for BrightFutures library, which is still the best helper for writing async code in Swift in my opinion.
If you enjoyed this article, please consider becoming a sponsor. My goal is to produce content like this and to work on open-source projects full time, every single contribution brings me closer to it! This also benefits you as a reader and as a user of my open-source projects, ensuring that my blog and projects are constantly maintained and improved.