Imperative FRP

From the imperative perspective the functional reactive programming paradigm can seem unnecessarily complex. Nacho Soto writes the same functionality using both paradigms to demonstrate how functional reactive programming helps write clearer, more concise and modular code in modern applications, using ReactiveCocoa in Swift.


My name is Nacho Soto. I work in Khan Academy with Andy Matuschak (mobile team, iOS and Android), and I am also building a small app Watch Chess (Android and iOS). Before Khan Academy, I built Elevate, and also a small app called Sayonara. Today I want to illustrate the differences between traditional imperative programming versus functional reactive programming. I will show why the paradigm is useful in modern applications by implementing example features using imperative programming and followed by method reactive programming improves the implementation.

Functional Programming — Glossary (1:03)

  • Functional programming: a paradigm the models computation as the evaluation of mathematical functions (called pure functions), without changing state or immutable data.
  • State: data over time. A function depending on time, however, creates complexity through when it will be called, and in what order will those be used. Without state, all that we need to take into account is the input, and how we transform it to receive an output.
  • Implicit state: state that is not clearly defined. For instance:
let x  = f()
print(x)
===> 1

let y = f()
print(y)
===> 2

Implicit state can be dangerous this way. Does it even make sense that calling the same function can return different things? Every time you have had to restart your computer, you have been a victim of state.

Get more development news like this

Simple vs. Easy (2:34)

Traditional imperative programing in state is easy. Something is easy when we are familiar with it. However, to achieve simplicity, few concepts need to be involved. Functional reactive programming might not be easy but that is not what it strives for. The goal is to make code simpler.

Complexity is a serious problem in software development because we tend to pick easy over simple, leading to disastrous consequences. Instead, we need to keep complex things (e.g. concurrency, asynchronous code) into simple code.

One of the challenges with functional programming is that applications need state. Without state we do not really have an application, and functional programming does not make that easy to model. That is where the functional reactive paradigm helps: unlike in imperative programming, this paradigm allows you to represent state by making it explicit. Time becomes a first-class citizen.

Imperative Programming Goes Wrong (Demo - 5:26)

I want to illustrate some of the common problems with traditional imperative programming by building a small Mac app: a dictionary (add or complete definitions when we type words).

[Nacho live-codes added functionality to the sample app, and demonstrates how adding more and more basics features creates insurmountable complexity. Specifically, he adds input throttling to the typing of words in the dictionary, retrying when API requests fail, and invalidating past requests. The code below is the result.]

Imperative Code: What’s Wrong (13:26)

We have all seen and written code like this. It does not seem bad because it is easy. I have written it really fast, but this is really not the way to write good-quality software.

private var lastScheduledWord: dispatch_cancelable_block_t?
private var lastRequest: NSURLSessionTask?

private var numberOfRetries = 0

func newSearchString(word: String) {
    cancel_block(lastScheduledWord)

    lastRequest?.cancel()
    lastRequest = nil

    lastScheduledWord = dispatch_after_delay(THROTTLE_SECONDS) {
        self.lastRequest = fetchWord(word, success: { result in
            self.displayResult(word, result)
        }, failure: { error in
            if self.numberOfRetries++ < NUMBER_OF_RETRIES {
                self.newSearchString(word)
            } else {
                self.numberOfRetries = 0
                self.displayError(error)
            }
        })
    }
}

The important questions we should ask ourselves about our code are:

Does it work?

There is really no way to know confidently if it is going to work in all cases.

Are there race conditions?

Again, we can’t be sure. We are going to have to consider which thread things are running to make sure that we do not have, for instance, concurrent access.

The Model in the Functional Reactive Method (14:55)

How can we add a new feature without adding (or multiplying) complexity?

We could try to model the same application, by modeling the flow of the data first. In our case, we are going to model a stream of data.

  1. The input: strings as the user is typing.
  2. Throttle those to avoid unnecessary requests.
  3. Make a request to the API.
  4. Retry if it fails.
  5. Receive the definition.
  6. Display it in the UI.

It looks simple, and the code implementation is even simpler. It’s exactly the same thing we have done in the imperative solution, but our flow of data simplifies it significantly.

The Better Way - Functional Reactive Code (Demo - 16:01)

[Nacho live-codes the same functionality using the functional reactive paradigm, with signals and certain useful RAC functions to significantly simplify the code. For instance, to retry the API request, he simply had to use the retry() function, and the invalidation took care of itself without added complexity. Same goes for throttling the input, and displaying the results.]

Simplicity of Functional Reactive Code (23:08)

let keyStrokes = searchField.keyStrokes()
keyStrokes
    .throttle(THROTTLE_SECONDS)
    .flatMap { word in
        fetchWord(word)
            .retry(NUMBER_OF_RETRIES)
            .takeUntil(keyStrokes)
            .map { result in (word, result) }
    } .start(
        next: displayResult,
        error: displayError
    )

I want to ask the same questions as before:

Does it work?

There may still be bugs (because it is run by a computer after all), but it now seems much more clear. We can look at the code and know what the programmers intent was with the code. However, if I go back to the previous example and show the code to someone, without any context, it is probably impossible to know this code is to work. Although it requires some knowledge of framework, we can now know what exactly these transformations are doing here and understand the programmer’s intent without any context.

Are there race conditions?

It does not matter anymore because we are not modifying data concurrently from different threads. We are just modeling the data as it goes through, and we do not have immutable state.

The most important thing is that we implemented new features (throttling, retrying, canceling requests) without adding more complexity or sacrificing readability.

Conclusion (24:55)

We have seen two examples of the exact same problem. The first one was, perhaps, easier to write. But we learned that our functional code was simpler. In a modern codebase of hundreds or thousands of files, the job of the engineer stands beyond contributing to the massive code. It should be our duty to make the code as manageable and scalable as possible. We can do so by reducing complexity, and FRP one great way to do so.

Credits (26:00)

I would like to give credit to these three talks; I highly recommend all of them.

Q&A (26:23)

Q: If you are trying to attempt this simplification in an Objective-C and Swift mixed codebase, do you have to have to use both versions of ReactiveCocoa?

Nacho Soto: RAC 3 (ReactiveCocoa) is the first version of ReactiveCocoa that does read in Swift, but the codebase actually includes both RAC 2 and RAC 3. You can still use ReactiveCocoa 2 and it includes some methods to convert from RAC signal to the new primitives, and vice versa.

Q: To avoid retain cycles in RAC 2 you had to do this weakify/strongify dance around self. For instance, displayError() in the code captures self. Does RAC 3 resolve this dancing?

Nacho Soto: I missed that retain cycle in the demo. You’re correct, we do capture self strongly and have a retain cycle. To avoid it in this case, we would probably use the takeUntil operator to keep the subscription alive until the view controller is going to be deallocated.

Q: The code looks clear and simple when it works as expected, but what happens when it doesn’t? How does one debug this code, uncovering what’s behind signals?

Nacho Soto: Certainly, debugging is one of the hardest parts of reactive programming because we do not have good tools to visualize what is actually going on. We can read the code and know exactly what we want it to do, but when it does not work, it is pretty hard. What I have done in the past to help me debug is doing things like printing values at the different steps in the flow to verify that it is actually doing the transformation that I am expecting, and to verify that values are actually going through and/or whether something got unsubscribed (or things like that). Printing for debugging is definitely awful; we should not be debugging things by printing things in the console. However, working with breakpoints is tough with RAC. I think someone who works at Khan Academy built a really cool tool to visualize streams, possibly for RxJS, and ideally we would have a similar tool for visualizing ReactiveCocoa streams.

Q: In that particular example functional reactive example, does the order of retry(), take(), and tell() matter?

Nacho Soto: It does not in this case. Whenever keystrokes meets a new value, that whole stream will unsubscribed. Nobody continues to listen to the stream after it is unsubscribed.

Q: Are there any special challenges with mixing ReactiveCocoa with imperative programming in the same view controller operating on the same data or fields?

Nacho Soto: Yes, certainly. It is not trivial to say, view controllers are not going to have data anymore; we will use streams for everything. Some people have tried doing this. You probably heard of the term MVVM; I think there comes a point where you hit a wall with UIKit and your architecture does not make it easy. Andy Matuschak talked about refactoring these out of your view controllers.

Nacho Soto

Nacho Soto works as a mobile engineer at Khan Academy, bringing their vast educational content to both iOS and Android devices. Previously, Nacho led the development of Elevate, Apple’s 2014 App of the Year. In his free time, Nacho enjoys chess, speed cubing, and diving deeper into functional programming.