Reducing the cognitive load with Drivers

Marko Božić
5 min readNov 29, 2017

--

Whenever I read someone’s code or try to understand an algorithm, the first thing I think about is how to structure the process to reduce the cognitive load.

What’s cognitive load? Straight from Wikipedia: In cognitive psychology, cognitive load refers to the total amount of mental effort being used in the working memory.

and what’s working memory? Again, Wikipedia says: Working memory is a cognitive system with a limited capacity that is responsible for temporarily holding information available for processing.

When understanding a piece of software, structuring the process usually means that I first focus on the software’s components — what they are and how they interact, leaving out all the implementation details — trying to understand how everything works in general. I dive into the details and implementations only after I’ve learned how the system works.

I’ve bolded “learned” because it’s important to really learn how the system works, not just understand it and leave it in your working memory — you need space in there for all the implementation details.

When thinking about algorithms, I first try to learn all the properties that are satisfied for a given structure or whatever I’m working with. Only after that do I work on their interconnection. Once you’ve learned about the basic properties of the structures, understanding more general properties of the algorithm is a lot easier.

Again, if you only understand the basic properties and leave them in your working memory, you’ve achieved nothing, because all the other properties that are easily deducible from the basic ones will not just pop into your head.

Let’s get back to our topic — Drivers. To fully understand the content of this article you need some background in RxJava, Kotlin, and Android. Although Android is not that important, the same concepts could be applied to different technologies (e.g., Spring 5 and the Reactor project)

We’ll start with a definition.

Definition 1. A Shared sequence is a safe observable with a specific sharing strategy observing on a specific thread. We say that an observable is safe if it can’t error out, i.e., the onError is never called.

Let’s take a look at the following code snippet.

The almostSharedSequence is almost a shared sequence. 😃 It has a specific sharing strategy share() and it observes on the AndroidSchedulers.mainThread() thread. The only problem is that the observable is not safe! If myMapper throws, the app crashes!

Anyway, in this text we’ll not talk about how to build a proper shared sequence, but about how to use a specific one called Driver. Let’s solve a particular problem.

Problem. Assume that you want to create a search input field which suggests results while you type. The field queries your server and displays a list of results and its size.

To make the example easier to follow we’ll mock our server request method in the following manner.

Attempt 1.

(1) We’re using the rxbindings to get the sequence of TextEdit’s text changes and map it to a sequence of strings.

(2) We throttle the sequence to prevent spamming.

(3) We use switchMap to unsubscribe from the previous observable (i.e., canceling the old request) and subscribe to a new observable (i.e., making a new request).

(4) We subscribe to the suggestions observable to get and display all the results.

(5) We subscribe to the suggestions observable to get and display the result size.

Although the solution looks readable, it’s far from correct. It will compile, but the app crashes as soon as you open it. Of course it crashes, we’re touching the UI on a computation thread (the thread getSuggestions subscribes on).

When working with the UI, you usually want to observe on the main thread! Only side-effects and complex computations should work on different threads.

Attempt 2.

(6) Touching the UI happens on the main thread, and the app doesn’t crash anymore (or does it?)

Well, it crashes when the getSuggestions throws an error because we’re not handling errors at all. Let’s fix it!

Attempt 3.

(7) When the error happens, continue with the empty list.

Now the app doesn’t crash! That’s because our suggestions observable is safe! That’s the property we want. However, if you input something in the search field, you’ll probably get some results. But, if you repeat the process several times, you’ll stop getting results. That’s because, when we get an error, we unsubscribe from the original observable and subscribe to a new one (Observable.just(listOf())) which completes immediately. This means that we’ve unsubscribed from RxTextView.textChanges(search_et) and no new strings are emitted.

When you think about it, RxTextView.textChanges(search_et) should be safe by design. The problematic observable is SuggestionsService.getSuggestions(it)! This is the one that should be safe!

Attempt 4.

(8) Now the network errors are handled and we never unsubscribe from the textChanges observable.

We’re not done. If you look closely, you’ll notice that the displayed result count doesn’t match the actual result count. And that’s because, when subscribing, we’re creating two different execution chains and every time a string is emitted, our function getSuggestions gets called twice! Usually returning two different result sets. We have to share our sequence.

Attempt 5. (Solution)

(9) Now, our observable is shared! Our mock method gets called once per text change, which is what we want.

Finally, we have a robust solution. Let’s think about what we have built and how we can reuse it.

We’ve built an observable with the following properties:

  1. it can’t error out, i.e., it’s safe
  2. observes on the main thread
  3. it’s shared among different subscribers.

What we have is a shared sequence called Driver.

Definition 2. A Driver is a shared sequence observing on the main thread with a sharing strategy .replay(1).refCount().

To make it reusable we abstracted it in a library: https://github.com/NoTests/SharedSequence.kt

Although it’s not hard to build a robust solution without the SharedSequence.kt library, by using it, we can guarantee at compile time that our Observables have all the desired properties. Let’s see what it looks like.

Attempt 6. (SharedSequence.kt library)

(1) We transform our observable into a driver. When doing so, we have to specify what happens when the observable errors out, thus making it safe!

(2) Driver’s switchMap lambda must return another shared sequence which means that the returning value is safe. To make it more explicit we call it switchMapDriver, meaning that its lambda returns a Driver.

(3) We know that the Driver observes on the main thread, no need to specify it explicitly. Note that the subscribe method is renamed to drive. In this way, when you see an observable with a drive method that compiles, you know that all the properties are satisfied.

Once you get comfortable with Drivers, you can stop worrying about all the problems we had while building a robust solution and focus only on how you’ll model your state, thus clearing up your working memory a bit. 😃

Its intended use case is to model sequences that manage (we propose a new term “drive”, hence the name Driver) your application or activity (e.g., state propagation with side effects).

You can find a working example in the repository and a bigger project (v.0.1.0) that uses Drivers here!

Drive safely!

--

--

Marko Božić

I'm a mathematician, manager, and software developer searching for great discussions.