Cover image
Mobile
13 minute read

Future-proof Your Android Code, Part 1: Functional and Reactive Programming Foundations

This tutorial explores the mathematical concepts behind two key programming paradigms, functional programming and reactive programming, so you can build long-lasting Android architecture.

Writing clean code can be challenging: Libraries, frameworks, and APIs are temporary and become obsolete quickly. But mathematical concepts and paradigms are lasting; they require years of academic research and may even outlast us.

This isn’t a tutorial to show you how to do X with Library Y. Instead, we focus on the enduring principles behind functional and reactive programming so you can build future-proof and reliable Android architecture, and scale and adapt to changes without compromising efficiency.

This article lays the foundations, and in Part 2, we will dive into an implementation of functional reactive programming (FRP), which combines both functional and reactive programming.

This article is written with Android developers in mind, but the concepts are relevant and beneficial to any developer with experience in general programming languages.

Functional Programming 101

Functional programming (FP) is a pattern in which you build your program as a composition of functions, transforming data from $A$ to $B$, to $C$, etc., until the desired output is achieved. In object-oriented programming (OOP), you tell the computer what to do instruction by instruction. Functional programming is different: You give up the control flow and define a “recipe of functions” to produce your result instead.

A green rectangle on the left with the text "Input: x" has an arrow pointing to a light gray rectangle labeled "Function: f." Inside the light gray rectangle, there are three cylinders with arrows pointing right: The first is light blue labeled "A(x)," the second is dark blue labeled "B(x)," and the third is dark gray labeled "C(x)." To the right of the light gray rectangle, there is a green rectangle with the text "Output: f(x)." The bottom of the light gray rectangle has an arrow pointing down to the text "Side effects."
The functional programming pattern

FP originates from mathematics, specifically lambda calculus, a logic system of function abstraction. Instead of OOP concepts such as loops, classes, polymorphism, or inheritance, FP deals strictly in abstraction and higher-order functions, mathematical functions that accept other functions as input.

In a nutshell, FP has two major “players”: data (the model, or information required for your problem) and functions (representations of the behavior and transformations among data). By contrast, OOP classes explicitly tie a particular domain-specific data structure—and the values or state associated with each class instance—to behaviors (methods) that are intended to be used with it.

We will examine three key aspects of FP more closely:

  • FP is declarative.
  • FP uses function composition.
  • FP functions are pure.

A good starting place to dive into the FP world further is Haskell, a strongly typed, purely functional language. I recommend the Learn You a Haskell for Great Good! interactive tutorial as a beneficial resource.

FP Ingredient #1: Declarative Programming

The first thing you’ll notice about an FP program is that it’s written in declarative, as opposed to imperative, style. In short, declarative programming tells a program what needs to be done instead of how to do it. Let’s ground this abstract definition with a concrete example of imperative versus declarative programming to solve the following problem: Given a list of names, return a list containing only the names with at least three vowels and with the vowels shown in uppercase letters.

Imperative Solution

First, let’s examine this problem’s imperative solution in Kotlin:

fun namesImperative(input: List<String>): List<String> {
    val result = mutableListOf<String>()
    val vowels = listOf('A', 'E', 'I', 'O', 'U','a', 'e', 'i', 'o', 'u')

    for (name in input) { // loop 1
        var vowelsCount = 0

        for (char in name) { // loop 2
            if (isVowel(char, vowels)) {
                vowelsCount++

                if (vowelsCount == 3) {
                    val uppercaseName = StringBuilder()

                    for (finalChar in name) { // loop 3
                        var transformedChar = finalChar
                        
                        // ignore that the first letter might be uppercase
                        if (isVowel(finalChar, vowels)) {
                            transformedChar = finalChar.uppercaseChar()
                        }
                        uppercaseName.append(transformedChar)
                    }

                    result.add(uppercaseName.toString())
                    break
                }
            }
        }
    }

    return result
}

fun isVowel(char: Char, vowels: List<Char>): Boolean {
    return vowels.contains(char)
}

fun main() {
    println(namesImperative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken")))
    // [IlIyAn, AnnAbEl, NIcOlE]
}

We will now analyze our imperative solution with a few key development factors in mind:

  • Most efficient: This solution has optimal memory usage and performs well in Big O analysis (based on a minimum number of comparisons). In this algorithm, it makes sense to analyze the number of comparisons between characters because that’s the predominant operation in our algorithm. Let $n$ be the number of names, and let $k$ be the average length of the names.

    • Worst-case number of comparisons: $n(10k)(10k) = 100nk^2$
    • Explanation: $n$ (loop 1) * $10k$ (for each character, we compare against 10 possible vowels) * $10k$ (we execute the isVowel() check again to decide whether to uppercase the character—again, in the worst case, this compares against 10 vowels).
    • Result: Since the average name length won’t be more than 100 characters, we can say that our algorithm runs in $O(n)$ time.
  • Complex with poor readability: Compared to the declarative solution we’ll consider next, this solution is much longer and harder to follow.
  • Error-prone: The code mutates the result, vowelsCount, and transformedChar; these state mutations can lead to subtle errors like forgetting to reset vowelsCount back to 0. The flow of execution may also become complicated, and it is easy to forget to add the break statement in the third loop.
  • Poor maintainability: Since our code is complex and error-prone, refactoring or changing the behavior of this code may be difficult. For example, if the problem was modified to select names with three vowels and five consonants, we would have to introduce new variables and change the loops, leaving many opportunities for bugs.

Our example solution illustrates how complex imperative code might look, although you could improve the code by refactoring it into smaller functions.

Declarative Solution

Now that we understand what declarative programming isn’t, let’s unveil our declarative solution in Kotlin:

fun namesDeclarative(input: List<String>): List<String> = input.filter { name ->
    name.count(::isVowel) >= 3
}.map { name ->
    name.map { char ->
        if (isVowel(char)) char.uppercaseChar() else char
    }.joinToString("")
}

fun isVowel(char: Char): Boolean =
    listOf('A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u').contains(char)

fun main() {
    println(namesDeclarative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken")))
    // [IlIyAn, AnnAbEl, NIcOlE]
}

Using the same criteria that we used to evaluate our imperative solution, let’s see how the declarative code holds up:

  • Efficient: The imperative and declarative implementations both run in linear time, but the imperative one is a bit more efficient because I’ve used name.count() here, which will continue to count vowels until the name’s end (even after finding three vowels). We can easily fix this problem by writing a simple hasThreeVowels(String): Boolean function. This solution uses the same algorithm as the imperative solution, so the same complexity analysis applies here: Our algorithm runs in $O(n)$ time.
  • Concise with good readability: The imperative solution is 44 lines with large indentation compared to our declarative solution’s length of 16 lines with small indentation. Lines and tabs aren’t everything, but it is evident from a glance at the two files that our declarative solution is much more readable.
  • Less error-prone: In this sample, everything is immutable. We transform a List<String> of all names to a List<String> of names with three or more vowels and then transform each String word to a String word with uppercase vowels. Overall, having no mutation, nested loops, or breaks and giving up the control flow makes the code simpler with less room for error.
  • Good maintainability: You can easily refactor declarative code due to its readability and robustness. In our previous example (let’s say the problem was modified to select names with three vowels and five consonants), a simple solution would be to add the following statements in the filter condition: val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5.

As an added positive, our declarative solution is purely functional: Each function in this example is pure and has no side effects. (More about purity later.)

Bonus Declarative Solution

Let’s take a look at the declarative implementation of the same problem in a purely functional language like Haskell to demonstrate how it reads. If you’re unfamiliar with Haskell, note that the . operator in Haskell reads as “after.” For example, solution = map uppercaseVowels . filter hasThreeVowels translates to “map vowels to uppercase after filtering for the names that have three vowels.”

import Data.Char(toUpper)

namesSolution :: [String] -> [String]
namesSolution = map uppercaseVowels . filter hasThreeVowels

hasThreeVowels :: String -> Bool
hasThreeVowels s = count isVowel s >= 3

uppercaseVowels :: String -> String
uppercaseVowels = map uppercaseVowel
 where
   uppercaseVowel :: Char -> Char
   uppercaseVowel c
     | isVowel c = toUpper c
     | otherwise = c

isVowel :: Char -> Bool
isVowel c = c `elem` vowels

vowels :: [Char]
vowels = ['A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u']

count :: (a -> Bool) -> [a] -> Int
count _ [] = 0
count pred (x:xs)
  | pred x = 1 + count pred xs
  | otherwise = count pred xs

main :: IO ()
main = print $ namesSolution ["Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"]

-- ["IlIyAn","AnnAbEl","NIcOlE"]

This solution performs similarly to our Kotlin declarative solution, with some additional benefits: It is readable, simple if you understand Haskell’s syntax, purely functional, and lazy.

Key Takeaways

Declarative programming is useful for both FP and Reactive Programming (which we will cover in a later section).

  • It describes “what” you want to achieve—rather than “how” to achieve it, with the exact order of execution of statements.
  • It abstracts a program’s control flow and instead focuses on the problem in terms of transformations (i.e., $A \rightarrow B \rightarrow C \rightarrow D$).
  • It encourages less complex, more concise, and more readable code that is easier to refactor and change. If your Android code doesn’t read like a sentence, you’re probably doing something wrong.

If your Android code doesn't read like a sentence, you're probably doing something wrong.

Still, declarative programming has certain downsides. It is possible to end up with inefficient code that consumes more RAM and performs worse than an imperative implementation. Sorting, backpropagation (in machine learning), and other “mutating algorithms” aren’t a good fit for the immutable, declarative programming style.

FP Ingredient #2: Function Composition

Function composition is the mathematical concept at the heart of functional programming. If function $f$ accepts $A$ as its input and produces $B$ as its output ($f: A \rightarrow B$), and function $g$ accepts $B$ and produces $C$ ($g: B \rightarrow C$), then you can create a third function, $h$, that accepts $A$ and produces $C$ ($h: A \rightarrow C$). We can define this third function as the composition of $g$ with $f$, also notated as $g \circ f$ or $g(f())$:

A blue box labeled "A" has an arrow, "f," pointing to a blue box labeled "B" that has an arrow, "g," pointing to a blue box labeled "C." Box "A" also has a parallel arrow, "g o f," pointing directly to box "C."
Functions f, g, and h, the composition of g with f.

Every imperative solution can be translated into a declarative one by decomposing the problem into smaller problems, solving them independently, and recomposing the smaller solutions into the final solution through function composition. Let’s look at our names problem from the previous section to see this concept in action. Our smaller problems from the imperative solution are:

  1. isVowel :: Char -> Bool: Given a Char, return whether it’s a vowel or not (Bool).
  2. countVowels :: String -> Int: Given a String, return the number of vowels in it (Int).
  3. hasThreeVowels :: String -> Bool: Given a String, return whether it has at least three vowels (Bool).
  4. uppercaseVowels :: String -> String: Given a String, return a new String with uppercase vowels.

Our declarative solution, achieved through function composition, is map uppercaseVowels . filter hasThreeVowels.

A top diagram has three blue "[String]" boxes connected by arrows pointing to the right. The first arrow is labeled "filter has3Vowels" and the second is labeled "map uppercaseVowels." Below, a second diagram has two blue boxes on the left, "Char" on top, and "String" below, pointing to a blue box on the right, "Bool." The arrow from "Char" to "Bool" is labeled "isVowel," and the arrow from "String" to "Bool" is labeled "has3Vowels." The "String" box also has an arrow pointing to itself labeled "uppercaseVowels."
An example of function composition using our names problem.

This example is a bit more complicated than a simple $A \rightarrow B \rightarrow C$ formula, but it demonstrates the principle behind function composition.

Key Takeaways

Function composition is a simple yet powerful concept.

  • It provides a strategy for solving complex problems in which problems are split into smaller, simpler steps and combined into one solution.
  • It provides building blocks, allowing you to easily add, remove, or change parts of the final solution without worrying about breaking something.
  • You can compose $g(f())$ if the output of $f$ matches the input type of $g$.

When composing functions, you can pass not only data but also functions as input to other functions—an example of higher-order functions.

FP Ingredient #3: Purity

There is one more key element to function composition that we must address: The functions you compose must be pure, another concept derived from mathematics. In math, all functions are computations that always yield the same output when called with the same input; this is the basis of purity.

Let’s look at a pseudocode example using math functions. Assume we have a function, makeEven, that doubles an integer input to make it even, and that our code executes the line makeEven(x) + x using the input x = 2. In math, this computation would always translate to a calculation of $2x + x = 3x = 3(2) = 6$ and is a pure function. However, this is not always true in programming—if the function makeEven(x) mutated x by doubling it before the code returned our result, then our line would calculate $2x + (2x) = 4x = 4(2) = 8$ and, even worse, the result would change with each makeEven call.

Let’s explore a few types of functions that are not pure but will help us define purity more specifically:

  • Partial functions: These are functions that are not defined for all input values, such as division. From a programming perspective, these are functions that throw an exception: fun divide(a: Int, b: Int): Float will throw an ArithmeticException for the input b = 0 caused by division by zero.
  • Total functions: These functions are defined for all input values but can produce a different output or side effects when called with the same input. The Android world is full of total functions: Log.d, LocalDateTime.now, and Locale.getDefault are just a few examples.

With these definitions in mind, we can define pure functions as total functions with no side effects. Function compositions built using only pure functions produce more reliable, predictable, and testable code.

Tip: To make a total function pure, you can abstract its side effects by passing them as a higher-order function parameter. This way, you can easily test total functions by passing a mocked higher-order function. This example uses the @SideEffect annotation from a library we examine later in the tutorial, Ivy FRP:

suspend fun deadlinePassed(
deadline: LocalDate, 
    @SideEffect
    currentDate: suspend () -> LocalDate
): Boolean = deadline.isAfter(currentDate())

Key Takeaways

Purity is the final ingredient required for the functional programming paradigm.

  • Be careful with partial functions—they can crash your app.
  • Composing total functions is not deterministic; it can produce unpredictable behavior.
  • Whenever possible, write pure functions. You’ll benefit from increased code stability.

With our overview of functional programming completed, let’s examine the next component of future-proof Android code: reactive programming.

Reactive Programming 101

Reactive programming is a declarative programming pattern in which the program reacts to data or event changes instead of requesting information about changes.

Two main blue boxes, "Observable" and "State," have two main paths between them. The first is via "Observes (listens for changes)." The second is via "Notifies (of latest state)," to blue box "UI (API in back end)," which goes via "Transforms user input to" to blue box "Event," which goes via "Triggers" to blue box "Function composition," and finally via "Produces (new state)." "State" then also connects back to "Function composition" via "Acts as input for."
The general reactive programming cycle.

The basic elements in a reactive programming cycle are events, the declarative pipeline, states, and observables:

  • Events are signals from the outside world, typically in the form of user input or system events, that trigger updates. The purpose of an event is to transform a signal into pipeline input.
  • The declarative pipeline is a function composition that accepts (Event, State) as input and transforms this input into a new State (the output): (Event, State) -> f -> g -> … -> n -> State. Pipelines must perform asynchronously to handle multiple events without blocking other pipelines or waiting for them to finish.
  • States are the data model’s representation of the software application at a given point in time. The domain logic uses the state to compute the desired next state and make corresponding updates.
  • Observables listen for state changes and update subscribers on those changes. In Android, observables are typically implemented using Flow, LiveData, or RxJava, and they notify the UI of state updates so it can react accordingly.

There are many definitions and implementations of reactive programming. Here, I have taken a pragmatic approach focused on applying these concepts to real projects.

Connecting the Dots: Functional Reactive Programming

Functional and reactive programming are two powerful paradigms. These concepts reach beyond the short-lived lifespan of libraries and APIs, and will enhance your programming skills for years to come.

Moreover, the power of FP and reactive programming multiplies when combined. Now that we have clear definitions of functional and reactive programming, we can put the pieces together. In part 2 of this tutorial, we define the functional reactive programming (FRP) paradigm, and put it into practice with a sample app implementation and relevant Android libraries.

The Toptal Engineering Blog extends its gratitude to Tarun Goyal for reviewing the code samples presented in this article.


Further Reading on the Toptal Engineering Blog:

Understanding the basics

The heart of functional programming is function composition, in which data is transformed from A to B, to C, to the desired output. It also has two other key elements: It is declarative, and its functions are pure.

Reactive programming is a programming pattern rooted in two concepts: declarative programming and reactivity.

Reactive programs respond directly to data or event changes instead of requesting information about changes.

Comments

Vladimir Shalimov
You put all the code into one function using 3 nested loops and then claim that imperative approach is error-prone and hard to read? This is not an issue of imperative programming, this is an issue of poor example. I've rewritten your original imperative sample. It is still 100% imperative, but much more easy to read and refactor. https://gist.github.com/PVoLan/246582b96529a333cee42b6f92c0e9af It is still longer than your declarative solution, though. But the reason for that is not the approach itself, but the fact that for declarative solution you use utility functions like <code>count</code> or <code>filter</code> instead of coding them manually
sswam
Thanks Iliyan, it's an excellent article and I'm looking forward to the second part.
Iliyan Germanov
Hi Vladimir, yes that's done on purpose and mentioned in the article: <i>"Our example solution illustrates how complex imperative code might look, although you could improve the code by refactoring it into smaller functions."</i> The point was to show that large-scale imperative code can get complicated quickly and because we can't put large examples in the article, I introduced this biased example. Imagine, the following routine situation in Android, requirements for a News app: - fetch news from API 1 and API 2 every 5 secs (which are paginated) - refersh News UI whenever newer news-feed items appear - the app must work in offline mode (caching) - the UI musn't be refreshed if the app is in background for more than 3 secs - all News containing blacklisted words must be filtered Here's a challenge you, do this 100% imperative and share the results with us! Then I can implement it using declarive approach and compore both solutions. Regarding utility functions, I agree they add some lines but they are 2-3 lines most each. Example `count`: ``` public inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int { if (this is Collection && isEmpty()) return 0 var count = 0 for (element in this) if (predicate(element)) checkCountOverflow(++count) return count } ```
Vladimir Shalimov
//The point was to show that large-scale imperative code can get complicated quickly Hey, if one have a goal to make code ugly and complicated, one can easily achieve it. Large-scale declarative code can become complicated quickly too, if developer doesn't care (callback hell?). But it doesn't make an imperative/declarative approach better/worse by itself. For example, the sample mentioned in "Declarative solution" sample is same overcomplicated for me as the "Imperative solution". Although it takes less lines, it is still the whole code fit into one function. I'd suggest extracting <code>uppercaseVowels()</code> function as a minimum (see here https://gist.github.com/PVoLan/f813214213e4ca7becd7f4bbf7d92907 ). Nested lambdas are no more better than nested loops. So, my point is that the idea of making things less complicated and more error-safe is not about imperative/declarative approach, but about correct division of the complicated task into a set of simple ones.
Vladimir Shalimov
// Imagine, the following routine situation in Android, requirements for a News app: I won't write it in code cause it would take a little bit more time than 5 mins, but I have a clear understanding of the architecture and how I'd implement it, if needed. The main challenge here is not the imperative complexity but the Android's lifecycle issues (as usual, though). Basically, we'll need - A Service to fetch the news and store it into Database (if you'd really need to fetch it <i>exactly</i> every 5 sec the background, what is a little bit inefficient by itself, that should be a Service. If business accepts another update technique, via notification or once in an hour or in foreground only, for ex, then we can use something else) - A Database to store cached news - An Activity to show the news from Database. A Service is going to be mostly imperative: infinite loop in the background where we synchronously load a newsfeed page, validate it, reformat if needed and put the database, then go for the next page, and so on. The timing is too important here, even with "every 5 sec" approach the network request would easily take 5 sec or more by itself, so to avoid network we should load stuff carefully and step by step An Activity would have a RecyclerView who fetches the data from Database. Here we'll most probably use LiveData and/or DataSources to properly handle Database updates and RecyclerView binding. Probably you'd call it "declarative", and you may be right, I see nothing wrong with that here.
MD Yusuf
I appreciate your help, Iliyan; this is a great post, and I'm eager to read more.
Iliyan Germanov
Thank you, Yusuf! I'm very glad that you find my article interesting!
Iliyan Germanov
Thank you! I'm very glad that you liked it!
Kevin Bloch
Thanks for your thoughts here, Vladimir. :) I think I see your point. At the same time, I wonder if there's anything inherent in an FRP approach that encourages developers to avoid things like nested lambdas, or at least makes it seem more of a natural fit. If what you're saying is true, a significant portion of the broader FP debate could be attributed more to psychology than technical merit, which would be interesting in its own way—the value of having to throw away bad habits by having a completely new mindset, instead of incrementally improving, especially for those who have been programming for longer under the imperative/declarative paradigms. (I'm not sure I agree, I'm just asking. ;) ) What do you think, Iliyan?
comments powered by Disqus