The Unreasonable Effectiveness of Declarative Code

Benjamin Encz at Swift Summit San Francisco, 2016

swiftsummit


Transcript:

Benjamin: Good afternoon, everyone. I hope you're still awake at this late time of the day. My name is Benjamin Encz. I'm a software engineer at Plangrid, here in San Francisco. We do software for the construction industry. Last year, I've also been working a little bit in open source software, mostly on a project called ReSwift. You can see it in the orange icon behind me.

Today, I only have a 10 minute speaking slot. That's a lightning talk. I decided I should probably pick a concise title that I can get through quickly. What I'm going to talk about today is The Unreasonable Effectiveness of Declarative Code and The Near Future of Programming. Joking aside, I really want to convey a simple idea. It is one that has informed the way I write code in the last few years, pretty significantly. That is the idea of embracing more declarative code. If you haven't heard the term declarative programming before, then bear with me. I'm gonna have a definition a little later on. First, I want to talk about why I think it is the near future of programming.

Ever since I started working as a professional software developer, I always wondered what the future of programming would look like. Especially in Objective-C, it felt like the code I was writing was very close to machine level. It wasn't very abstract. Working on multiple apps, it seemed like I was repeating the same work over and over again. I was wondering, what do the professional developers out there do. What is the next tool gonna look like, that's gonna make it a lot easier and much more effective to build software? I quickly realized that it's a naive view of how things are gonna evolve in the future. Most of the ideas for the future of programming are pretty far out there. They don't really apply to our day to day work.

In the extreme case, looking far, far in the future, we have artificial intelligence replacing all of us. That's probably not going to be too relevant for us, though. In the near future, people are actually working on these ideas. We have tools like Eve, that you might have heard about in the last few weeks. It's a literate programming environment. We have ideas from people like Bret Victor, who proposes to build a code, basically, of visual tools and graphs, instead of with code, and write software that way. These ideas are not applicable for iOS development. They don't help me in my day to day programming.

Then, two years ago more or less, I found a sweet spot. That is declarative programming. It can be adopted step by step. You can use it in your iOS code base today. It really feels like a big improvement over what I've been doing before. I discovered it mostly for tools that I enjoyed using that provided declarative APIs, which made it easy to write declarative code. You can see two examples here. The top one is React, which is a JavaScript framework, so you might not be familiar with it. It comes out of Facebook. It is a declarative way of building user interfaces. It's kind of the counterpart of UIKit for the web. If you've ever worked in React and UIKit, you would have noticed that working in React is a lot easier to build complex user interfaces. Most UI's can be built in a fraction of the time. You're gonna be a much more effective developer. Closer to home, there are also tools that I like to use, like ReactiveCocoa, which provides a tool for functional reactive programming. That also allows for declarative programming. We're gonna see an example in a second.

Now you know how I got to declarative programming, but what does it actually mean? What is the definition? I want to first start with a tagline, because I think it's easier to remember that. To me, writing declarative code is about stating facts, instead of stating behavior. We're going to go into an example and a better definition in a second. This is the core of what it means to me. Here's an example of exactly that in ReactiveCocoa. You don't have to understand the framework. It's pretty simple code. I’m gonna go over it. What we're using here is a custom operator that ReactiveCocoa provides. That custom operator allows us to create a binding. This binding says that the UI component that you can see on the left, the label, will update whenever the property on the right-hand side updates. We have a usernameLabel. Whenever the user name updates, the label will also update.

This is a very simple example of declarative code. What we're doing is we're stating a fact. We're saying these two properties are coupled together. We don't actually implement the behavior of doing that. The imperative counterpart would be writing code that observes the property, then calls UI and manually updates it. It would be harder to understand. This is a very simple example, but there is also a more formal definition of what declarative code is.

It's really hard to come by because programming language theorists tend to argue about it, but I found a blog post that I enjoyed. I link to it at the end of the talk. It has, basically, four properties that it says all declarative code should have. The first one is declarative code is idempotent. That means we can state these facts multiple times, without changing the behavior of our program. We could establish one UI binding in multiple places, and it wouldn't affect the outcome of the program.

Declarative code is also commutative, which means we can reorder these declarative statements without that it changes how a program behaves. They're also concurrent. This one is a little bit more difficult to understand. It essentially means that these different facts that we state in our program hold true at the same time. We have multiple bindings that are established and they're all true concurrently, at the same time. Whereas when we think about imperative code, we have to think about a sequence of instructions and at which point of the instructions we currently are. Lastly, declarative code is reactive, which means, while the definition of the binding between the property and the UI label is static, the meaning of that binding can change over time. As the property changes, the UI will update with it.

That's a very theoretical way of looking at it. What it means is that declarative code is often easier to reason about and easier to write. If you think just about one example, declarative code being commutative. It means we can reorder the statements without changing the program's output. That means we don't have to think about the ordering of statements to understand how the program works. That means it's easier to understand in the first place.

How would you go about writing your own declarative code? I talked about libraries that I like to use. Actually in Swift, it's very easy to write your own declarative APIs. I want to give you an example out of the Plangrid app. I'll have to give you some background on what we built, so you can understand the next slide. In the Plangrid app, an important part of what we do is we provide offline functionality, which means we need to synchronize a lot of data from the server. We have different types of objects. We have blueprints annotations. We need to sync them. The synchronization for each of them looks slightly different. They have different endpoints. They have different ways how they get parsed and persisted. We need a way to implement this download behavior in various different places.

In Objective-C, we used to have a base downloader that would implement most of the downloaded behavior. Then, we had various sub-classes that changed the behavior for the individual types. We'd have a blueprint downloader, an annotation downloader and so on. When we moved this code to Swift, we decided we want to move it into a more declarative style. That meant we implemented a generic downloader that has all the generic behavior. Then, we have types that configure that downloader to their needs. Now, the annotation and blueprints no longer implement their own downloader. Instead, they implement a protocol that declaratively configures how the downloading for them works. It's a fairly complex process because the downloading has to perform delta synchronization, which means we only fetch data that we didn't get from the server before. We need to support pagination because sometimes the responses get so big that we can't fetch all of them at once, so we need to fetch them in multiple pages. Lastly, we need to issue statements to the database to update our local state to match what the server told us.

All of this now fits into an e-protocol definition I'm gonna show you. The protocol has a few more methods, but the most important one of the DownloadSyncable protocol is the handleServerResponse method. This method is the declarative implementation that states how a certain type gets synced down onto the device. We're only going to focus on the interface because it's the most important aspect. This interface is a static method, which means most importantly this method cannot rely on any state of any instance. It is a static mapping. It's something like a pure function that only maps inputs to outputs. The inputs to this function is the response that we got back from the server. This is any headers that we got back, status codes, the body. In our case, it's mostly JSON.

We also get back the requests that we originally sent. That is sometimes important to understand how to treat the response. We get an optional SyncRequestState. The SyncRequestState is a custom object that each type can provide to remember important information about the sync process as it goes on. It can be a last timestamp. It can be any kind of information that we need in order to pick up synchronization in the future.

Then, we have three return values. The first one is a set of changes that we want to update in the database. Objects that we need to create, delete or update. That is the first important part of this method signature. Here we don't talk to database directly as part of our synchronization. Instead, we declaratively say what are the changes we need to do to the database. As ModelChange, is just an enum that says create this object, update another object or delete a certain object. It doesn't talk to the database and doesn't perform any side effects.

Secondly, we returned a newState. Once again, if we need to remember anything about the synchronization process, like a timestamp, we can return that here. Lastly, we have an optional nextRequest return value. That one allows us for pagination, to tell the downloader that we need to fetch more information. If we, based on response, find out that we need to get another page of data, then we can return the description of the nextRequest as part of this method. The downloader will take the request, send it to the server, and, once the response comes back, it will again invoke the handles to the response method.

This was a very declarative way of implementing the download functionality for various different types. It has a bunch of advantages. Firstly, as I mentioned earlier, we have facts instead of behaviors. It's much, much easier to reason about. The average implementation for this protocol method that I just showed you is somewhere between 30 to 50 lines because it doesn't do all that much. That is for assuming it does pagination, delta synchronization and communication with the database.

The interface itself also forms a kind of DSL. We have input types. We have output types that are well defined. We know that the function has to be stateless. All these constraints make it really hard to come up with various different implementations of this method. Mostly, there's only one right way to implement it. That means that as you adopt this protocol throughout the code base, your code base becomes more uniform and easier to understand for other developers.

You also have really great separation of concerns because this download synchronization code doesn't talk to the database. It doesn't talk to the network layer, but instead declaratively returns descriptions of requests or model changes. We don't have to know at all how these are implemented. There are some other machinery that talks to the database and takes these instructions, actually implements them, but the download process itself is totally ignorant of them. That means they're entirely decoupled. This interface is really great to test. Any declarative interface is simple to test because it just takes inputs and maps them to outputs. Unit tests are extremely simple to write.

Overall, I feel like this approach has moved me just a tiny bit into the future of programming. I hope the core idea is useful to you, too. It's really hard to convey in a 10 minute speaking slot so I have a blog post that goes a little bit more into detail. I also link to the definition of declarative programming that I gave earlier. Thanks a lot.