authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Iliyan is an Android developer and CTO who has founded four startups and created several top-rated apps, including Ivy Wallet, which has received 10 YouTube tech community “best UI/UX” awards. He specializes in functional programming, UX, Kotlin, and Haskell.
PREVIOUSLY AT
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 (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.
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:
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.
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.
First, let’s examine this problem’s imperative solution in Kotlin:
fun namesImperative(input: List): List {
val result = mutableListOf()
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): 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.
isVowel()
check again to decide whether to uppercase the character—again, in the worst case, this compares against 10 vowels).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.Our example solution illustrates how complex imperative code might look, although you could improve the code by refactoring it into smaller functions.
Now that we understand what declarative programming isn’t, let’s unveil our declarative solution in Kotlin:
fun namesDeclarative(input: List): List = 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:
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.List
of all names to a List
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.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.)
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.
Declarative programming is useful for both FP and Reactive Programming (which we will cover in a later section).
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.
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())$:
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:
isVowel :: Char -> Bool
: Given a Char
, return whether it’s a vowel or not (Bool
).countVowels :: String -> Int
: Given a String
, return the number of vowels in it (Int
).hasThreeVowels :: String -> Bool
: Given a String
, return whether it has at least three vowels (Bool
).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
.
This example is a bit more complicated than a simple $A \rightarrow B \rightarrow C$ formula, but it demonstrates the principle behind function composition.
Function composition is a simple yet powerful concept.
When composing functions, you can pass not only data but also functions as input to other functions—an example of higher-order functions.
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:
fun divide(a: Int, b: Int): Float
will throw an ArithmeticException
for the input b = 0
caused by division by zero.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())
Purity is the final ingredient required for the functional programming paradigm.
With our overview of functional programming completed, let’s examine the next component of future-proof Android code: reactive programming.
Reactive programming is a declarative programming pattern in which the program reacts to data or event changes instead of requesting information about changes.
The basic elements in a reactive programming cycle are events, the declarative pipeline, states, and observables:
(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.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.
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.
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.
Sofia, Bulgaria
Member since March 30, 2022
Iliyan is an Android developer and CTO who has founded four startups and created several top-rated apps, including Ivy Wallet, which has received 10 YouTube tech community “best UI/UX” awards. He specializes in functional programming, UX, Kotlin, and Haskell.
PREVIOUSLY AT
World-class articles, delivered weekly.
World-class articles, delivered weekly.
Join the Toptal® community.