The asynchronous code we’re used to writing in Objective-C has a lot of problems. It’s not great for handling errors, and it doesn’t scale well when you find yourself composing many asynchronous functions to create new asynchronous functions. In this talk, Javier Soto demonstrates how we can implement a Future
type in Swift that can greatly simplify our asynchronous APIs. Building on a Result
type, we go via map
and andThen
, ending on a suggestion that, for Swift, the future of Futures might be signals, and ReactiveCocoa 3.
To see the code used in this talk, Javi has made a Playground, which you can view on GitHub.
Doing Things Better with Swift (0:00)
Thank you everyone. I’m super happy to be up here talking to you all about Swift. Thanks a lot to the organizers for putting this together and having me here. My name is Javi. I go by @Javi on Twitter, and I’m an iOS engineer at Twitter.
There are a lot of things that we can do better in Swift than we could do before in Objective-C. We’ve already seen a couple examples of those in the previous talks. And when I continue with that train, I’m going to talk about one design pattern that I really like that we can bring to Swift. It will make our code a lot better.
Asynchronous APIs Today (1:19)
As Chris Eidhof mentioned, I’m also going to avoid saying words that start with “M”, and I’m going to keep it really practical. We’re going to see how we traditionally do asynchronous APIs, and we’re going to see a couple of problems with that approach, especially in regards to error handling. Then I’ll introduce a simple Future API, we’ll see how we could implement that, and how it helps improve our asynchronous code.
Let’s talk about the problem. What does this code look like today?
struct User { let avatarURL: NSURL }
func requestUserInfo(userID: String, completion: (User?, NSError?) -> ())
func downloadImage(URL: NSURL, completion: (UIImage?, NSError?) -> ())
func loadAvatar(userID: String, completion: (UIImage?, NSError?) -> ()) {
requestUserInfo(userID) { user, error in
if let user = user {
downloadImage(user.avatarURL) { avatar, error in
if let avatar = avatar { completion(avatar, nil) }
else { completion(nil, error!) }
}
} else { completion(nil, error!) }
}
}
This is Swift code that’s written in the same way we write Objective-C asynchronous APIs. There is a bunch of nested callbacks. There is a load avatar function that, given the userID
string, tries to download an avatar. You pass the completion block that will be called with an image or an error, and this function doesn’t really have any logic. It doesn’t know how to do it, so what it composes itself with smaller functions that also do other things asynchronously. It uses the requestUserInfo
function to retrieve the user asynchronously, and the download image function. The way it does that is by nesting those calls within each other. Every time there is an error it has to check explicitly, build out and call the completion block.
This may look fine to most of you who are very used to this, but this pattern doesn’t really scale. It’s very easy to end up here, and I’m sure you have seen this, even if you don’t do PHP. So let’s avoid it, we can do better. However, there are some other even more specific problems with this code. One of the asynchronous APIs that takes a completion block like this will look at the types. The completion block takes a tuple, or two values; they’re both optional because it’s possible that the download fails, or it’s possible that it doesn’t fail and then there is no error.
Those question marks you see, what is important to understand is that they’ve always been there, we’ve just never seen them. They were implicitly always there in Objective-C, because any point or type could carry a nil value.
If we look at all the possible combinations that can come in that Optional, we find that there are two. There are the ones that we expect: either there is an image and no error, or no image and an error. What’s funny is that there are another two cases that are totally possible, and don’t really make a lot of sense. What is my app supposed to do with an image and an error, what should I make out of that? And what if I don’t have an image but I don’t have an error either? What happens?
The Importance of Handling Errors (3:42)
I always like to say how I see computers as perfect machines that never make any mistakes. And whenever we encounter bugs, there has been a difference between what the programmer thought they told the computer to do, and what they actually told the computer to do. Where we don’t have a great way to express what we want to accomplish, we’re more likely to tell the computer to do the wrong thing.
I think, particularly in regards to error handling, this is very important. We need to handle errors appropriately in our applications. This sort of API lets you misuse it in many ways, for example, checking the error first as opposed to checking the value first. That’s an anti-pattern. Foundation tells you you should do it the other way around, in case there’s an error but it was actually a success.
I Hate NSError (4:55)
And then there is NSError
. I hate NSError
, but I can’t really blame Apple because, to be honest, it’s the best that you can do in Objective-C. Let’s see one example of why NSError
is problematic.
var error: NSError?
let string = NSString(contentsOfFile:path encoding:NSUTF8Encoding error:&error)
if string == nil {
// Oops:
if error.code == NSFileReadNoSuchFileError {
// ...
}
else if ...
}
This code is wrong, but it’s not immediately obvious at all. We’re calling a Foundation API. If we look at the documentation, it makes absolutely no reference to what kinds of errors we can find inside the error that we get in return. It doesn’t mention the domain, and it doesn’t mention the error codes. What we’re doing here is checking the error code without first checking the domain, and that doesn’t really make sense. There can be two error codes with the same value in different domains, we would be making the wrong assumption here. Another thing that we can do with this API is pass nil in the error parameter, and just tell Foundation, “I don’t care about errors”, but I think that robust software requires us to take really good care of errors.
It’s not surprising that in the software that we use every day we find crazy things like this, and we can’t really blame developers. If our tools don’t make it easy and convenient to handle errors, we’re just going to be lazy. We’re going to pass nil
to that method. We’re going to ignore the errors. We’re not going to be able to know which errors are there, so we’re just going to bring them to the console and move on.
A Proposal (6:40)
This is what I propose: Instead of using NSError
, we can use our own specific types that encapsulate the types of errors that we can encounter in our APIs. We can create a protocol that all those types can conform to and participate in being errors.
protocol ErrorType { }
enum UserInfoErrorDomain: ErrorType {
case UserDoesNotExist
case UserRequestFailure(reason: String)
case NetworkRequestFailure(reason: String)
}
extension NSError: ErrorType { }
For example, if we have an API that does some sort of thing with users, we can declare an enum that conforms to this protocol and has three very clear error cases. Perhaps it can provide even more additional information, like a reason string — we’ll see in a second how we’ll use that. If we really want to use NSError
, we can also make NSError
participate in this.
Another really cool thing that we can do with this, and it may seem contradictory at first, is that we could even create a NoError
type.
enum NoError: ErrorType { }
let error = NoError(?)
Weird, right? The cool thing about this type is that, because it’s declared as an empty enum, we actually cannot create a value of that type, because it doesn’t have any constructor. So now you say that’s even more useless. But precisely for the reason that we know that null values can be constructed. If we have an API that tells us the type of errors that it’s going to carry, is type NoError
, we know that the API is fallible, it can fail. What’s important about this is that we actually don’t need a unit test to prove it, the compiler can verify that for us.
Building on a Result Type with Future (8:22)
We’re going to build on top of a Result
type, as we saw in Brian’s talk earlier. Result
can represent either a value, in the case of a success or an error — but not both things at the same time — or neither. I’m cheating a little bit on this slide; This is currently not possible in Swift because of compiler limitation. You have to make use of a workaround like the Box class. But I chose to eliminate it from the slides because it’s kind of noisy. It’s not really important and it reads better this way.
enum Result<T, E: ErrorType> {
case Success(T)
case Error(E)
}
So let’s look at Future
. How can we implement this? Hopefully by looking at how the implementation works we’ll understand the value, and it’s also a lot of fun! So what’s Future
? You may have heard of them with another name perhaps in some other programming languages or frameworks. They’re known as Promises. They’re essentially the same thing.
struct Future<T, E: ErrorType> {
typealias ResultType = Result<T, E>
typealias Completion = ResultType -> ()
typealias AsyncOperation = Completion -> ()
private let operation: AsyncOperation
}
Futures encapsulate different computation, which is a very fancy way of saying that they abstract the work required to retrieve some value that occurs in some delay; for example, a network latency or something like that. But, you could say, “Oh, that’s just like closures, right?”, or, “That’s just like callbacks!”. But the great thing about Futures is that we can make an API that allows us to treat those Futures as if they were the actual values that they carry. So we can manipulate them, we can transform them, we can concatenate them, and we eliminate all the noise related to the asynchrony itself. That simplifies our code a lot.
Implementing Future
(10:16)
Let’s start implementing it. We can create a struct, and the struct is generic over T
and E
like Result
, because it’s essentially going to use Result
. I have defined a couple of type aliases to help us with the types. Result
type is what we just saw, and we’re going to have a completion type that’s going to be invoked when you tell Future
, “Now okay, give me the value”. You’re going to pass the completion block so it takes the Result type and doesn’t return anything. The asynchronous incorporation is the closure that will be called to start the work to return the value, so that’s going to be called with a completion block that it will call when it’s done, and it doesn’t return anything. Future
encapsulates the operation, and we can instantiate it by passing that operation. The important bit of the public API is that start method, that’s what allows the client of the API to tell Future
, “Okay now retrieve the value and give it to me when it’s done.” But so far it is not that useful. It looks a whole lot like completion blocks except for the use of Result
.
struct Future<T, E: ErrorType> {
init(operation: AsyncOperation) {
self.operation = operation
}
func start(completion: Completion) {
self.operation() { result in
completion(result)
}
}
}
map
& andThen
(11:15)
What’s really cool about Future
is when we implement some operators on them. I want to implement map
and andThen
, so let’s go with map
first. This is what we would accomplish with a map
function. We have a user ID string and we want to get the URL for the avatar. We create a function that returns that, and we have another function already that downloads the user information and gives us this really simple user struct, which is a URL. If we want to transform one into the other instead of nesting callbacks, we can just map
it and transform it by returning the other material from inside the user.
struct User { let avatarURL: NSURL }
func requestUserInfo(userID: String) -> Future<User, ErrorDomain>
func requestUserAvatarURL(userID: String) -> Future<NSURL, ErrorDomain> {
return requestUserInfo(userID)
.map { $0.avatarURL }
}
This is what the type signature looks like. I think it’s interesting to compare it to the map
function signature, and, for example, all their two types in the Standard Swift Library, Array and Optional. If we look at the types, they are exactly the same. They’ll take a function that knows how to get a value of type U
giving a value of type T
. Which T
is the type of values inside that container, and U
will be the type of the values in the new container.
struct Array<T> {
func map<U>(f: T -> U) -> [U]
}
enum Optional<T> {
func map<U>(f: T -> U) -> U?
}
struct Future<T, E: ErrorType> {
func map<U>(f: T -> U) -> Future<U, E>
}
Implementing map
on Future
(12:44)
So let’s implement it. We can create a new Future
because it has to be of type U
, so we need a new Future
. This is how we substantiate it, like we saw, passing an operation. In order to transform the value, we first need to get the value, so we call start and we get the result. This result can either be a success or a failure, so we need to switch on it.
func map<U>(f: T -> U) -> Future<U, E> {
return Future<U, E>(operation: { completion in
self.start { result in
switch result {
case .Success(let value):
completion(Result.Success(f(value)))
case .Error(let error):
completion(Result.Error(error))
}
}
})
}
Let’s go over the success case. The success case, like the type signature, tells us we need to get the value transformed with the F
function — we’ll run it again in a success case and we’ll call the completion block. The error case is even simpler. We don’t have a value, we can’t transform it, we just need to call the completion block with the same error. This is what it looks like in one slide.
This short circuiting that happens in the error cases essentially is the same that Brian showed with the Railway Oriented Programming concept. But now let’s say that we want to do something with the value that the initial Future
gives us, but we want to retrieve a new value given that. Retrieving that value is not as easy as calling the avatar URL. Retrieving that value also incurs some delay, we need another network request, for example.
Declaring andThen
(13:48)
This is not going to work because we need to pass a function that returns another Future. So we can declare a Future and an andThen
function on our Future API. We would use it like this, for example: If we have a request user avatar URL like we just saw, we can now return the Future of your image by concatenating the download image operation after that. We just call andThen
and pass the other function.
func requestUserAvatarURL(userID: String) -> Future<NSURL, ErrorDomain>
func downloadImage(URL: NSURL) -> Future<UIImage, ErrorDomain>
func downloadUserAvatar(userID: String) -> Future<UIImage, ErrorDomain> {
return requestUserAvatarURL(userID)
.andThen(downloadImage)
}
This is what the signature looks like. Similar to map
, the important difference is that, like I said, the F
function doesn’t return a U
. It returns a new Future. Implementing it is very, very similar. Again we create a Future, we started the initial one to retrieve the value, and we switch on the result. On the success case now, when we call F
on the value we don’t have a value that we can return. Now we have another Future, so we need to start it, and when we’re done that’s the final result.
In the error case, the same thing, we have to short circuit it. If we don’t have a value, we can’t transform it to continue doing more operations with it. We need to call the completion block immediately. It is the whole implementation, and you’ll realize that it looks a lot like map
. In fact it’s interesting that you can implement one in terms of the other, but I chose to implement them separately to see what they have to do.
So let’s see what we’ve accomplished. We went from a nested callback mess that has to explicitly handle errors in every single call, to a declarative function that tells us exactly what we need to do to retrieve this avatar: We need to request user info, map
it into the avatar URL, and then download the image. It’s not about lines of code here, it’s about our ability to express our intentions in the code. But the great benefit of this as well is that, if you look at the function on the bottom, there is no error handling. All the errors get carried through automatically, as soon as they happen.
func andThen<U>(f: T -> Future<U, E>) -> Future<U, E> {
return Future<U, E>(operation: { completion in
self.start { firstFutureResult in
switch firstFutureResult {
case .Success(let value): f(value).start(completion)
case .Error(let error): completion(Result.Error(error))
}
}
})
}
Future Limitations (16:45)
So hopefully you’ll like Futures as much as I do and they’ll seem as useful as I think they are. But it’s important to understand a few limitations of them. The example that we saw is very common in our mobile applications. We have a network request, some sort of asynchronous request like that. But that’s not the only type of asynchronous thing that we deal with. And Futures failed to represent all their abstractions that require sending more than one value through. This Future can only retrieve one value, but sometimes we deal with things like user interactions, like a gesture recognizer, that will send many different values over time.
Another difference with other APIs that we deal with is that they’re not consumer-driven like the one that we just saw. You don’t tell the gesture recognizer, “Okay, now recognize the touch.” It’s the user touching the screen. It’s the producer pushing values onto the screen.
Signals (18:08)
The good news is that there’s a better abstraction in Futures. Signals allow you to represent all of those things. Signals are a very powerful tool, they’re in a way a superset of Future, and therefore they’re a very, very useful abstraction.
The next great thing is that we don’t have to implement them. ReactiveCocoa, which you may have heard of implements signals and allows you to build your app using this abstraction. ReactiveCocoa 3 is written in Swift, and you can check it out on GitHub. But Colin, right after me, is going to talk to you all more about that.
The slides may go a little fast but I made a playground with all the code available on my GitHub repository. I encourage you all to download it, play with it.
And with that I’m happy to take questions.
Thank you everyone.
Q&A (19:05)
Q: Why did you decide to implement Future with both the type and the error implementing the NoError
class, instead of allowing the programmer to do a Future operation? You could implement Future, which takes only one generic type, and then that generic type can be Result
, so you don’t have to specify NoError
as the type of the error.
Javi: That’s a good question. The thing with that is that it’s because we use Result
that the implementations of map
and andThen
know how to short circuit errors, right. So if we make it a generic and we make it work with things that are not Result
, map
and andThen
are not going to know what’s a failure and what’s a success. Does that make sense?
Q: Why did you call it andThen
instead of flatMap
?
Javi: That’s a good question. Some of you may realize that the indent function is also known as flatMap
. That’s true. The reason why I called it andThen
is because I think it expresses better the meaning and what we’re using it for. flatMap
is now in the Swift Standard Library so I think we’ll start seeing it more and more, but I wanted to introduce the fewest functional programming terms in this talk, so that’s why I decided not to use it.
Receive news and updates from Realm straight to your inbox