- Functional Programming for Android Developers — Part 1
- Anup Cowkur
- The Nuggets translation Project
- Translator: skyar2009
- Proofread by: Danny1451, yunshuipiao
I recently spent some time learning Elixir, an excellent programming language for beginners.
I wonder, why don’t we use the ideas and techniques of functional programming in Android development?
When most people hear about functional programming, they think of Hacker News posts about monads, higher-order functions, and abstract data types. It seems to be an arcane realm far removed from the usual hard coding programmers, and only the domain of powerful hackers.
Leave it alone! I would say you can learn it, you can use it, and you can build beautiful applications with it — elegant, readable, bug-free code.
Welcome to How Android Developers program functionally (FP). In the next series of articles, I’ll walk you through the basics of FP and how to use FP in older versions of Java. This article is intended to be practical, and academic remarks will be kept to a minimum.
FP is a big topic. We’ll cover only ideas and techniques that are useful for writing Android code. You may see some ideas that are not directly applicable for completeness reasons, but I will try to keep the material as relevant as possible.
Are you ready? Let’s get started.
What is functional programming? Why should I use it?
That’s a good question. Functional programming is an umbrella for a set of programming ideas that have been treated unfairly. At its core, it is a way of programming that treats a program as an evaluation of a mathematical method, with no state changes and no side effects (which we’ll talk about in a moment).
The core idea of FP emphasizes:
- Declarative code – the programmer should care what it is, and let the compiler and runtime environment care how it is done.
- Clarity — The code should be as obvious as possible. In particular, isolate side effects to avoid accidents. To clearly define data flow and error handling, avoid GOTO statements and exceptions because they can put your application in an unexpected state.
- Concurrency – Because of the pure function concept, most functional code is parallel by default. Since CPUS are not running as fast as they used to year after year (see Moore’s Law), it is generally believed that this trait has led to the growing popularity of functional programming. And we must also take advantage of the multi-core architecture to make our code as parallel as possible.
- Higher-order functions – Functions, like other language primitives, are first-class citizens. You can pass functions like strings and ints.
- Immutability – variables cannot be modified once initialized. Once created, it will never change. If changes are needed, new ones need to be created. This is another aspect of clarity and avoiding side effects. If you know a variable can’t be changed, you’ll be more confident about its state when you use it.
Isn’t declarative, specific, and concurrent code easier to derive and designed to avoid surprises? I hope I’ve piqued your interest.
As part one of this series of articles, we start with some basic concepts of FP: purity, side effects, and sequencing.
A function is pure when its output depends only on the input and has no side effects (which we’ll talk about shortly). Let’s look at an example.
A simple function that sums two numbers. One number is read from the file and the other number is the parameter passed in.
int add(int x) {
int y = readNumFromFile();
return x + y;
}
Copy the code
The output of this function depends not only on the input, but also on the return from readNumFromFile(), which may have different outputs for the same input parameter x. This function is not pure.
So let’s make it a pure function.
int add(int x, int y) {
return x + y;
}
Copy the code
The output of the function now depends only on the input. For a given x and y, the function always returns the same output. This function is pure. Similarly with mathematical functions, the output of a mathematical function depends only on its inputs — which is why functional programming is more like mathematics than the way we normally program.
P.S. no input is an input. A function is a pure function if it has no input and its return is always the same.
P.P.S. The property that the input always returns the same output is also referred to as reference transparency, which you might encounter when talking about pure functions.
Side effects
To explore this concept, let’s modify the original function to store the results in a file.
int add(int x, int y) {
int result = x + y;
writeResultToFile(result);
return result;
}
Copy the code
This function writes the result to a file, which modifies the state of the outside world. So the function has side effects, it’s not pure anymore.
Any code that modifies external state (modifying variables, writing files, storing DB, deleting content, etc.) has side effects.
Functions with side effects should be avoided in FP because they are no longer pure functions but depend on historical context. The context of the code is not determined by itself, which makes it harder to derive.
Let’s say you write a piece of code that depends on the cache. The output of the code depends on whether someone has written to the cache, what was written to it, when it was written, whether the data written is valid, and so on. You can’t know what your program is doing unless you know all the possible states of the cache it depends on. If you expand your code to include everything your application depends on — networks, databases, files, user input, and so on — it becomes hard to know exactly what’s going on and to consider everything at once.
Does this mean we don’t use networks, databases, and caches anymore? Of course not. When the execution is done, the application often needs to do something. For Android apps, for example, it’s often about updating the UI so that users actually get useful content from our apps.
FP’s greatest concept is not to abandon side effects altogether, but to embrace and isolate them. We keep side effects at the edge of the system to minimize the impact, make the application easier to understand, and avoid messing up the application with side effects functions. We will discuss this in detail in a later article in this series when we examine functional architectures for applications.
If we have several pure functions with no side effects, the order in which they are executed is irrelevant.
Let’s take an example. We have a function that calls three pure functions:
void doThings() {
doThing1();
doThing2();
doThing3();
}
Copy the code
We explicitly know that these functions are not dependent on each other (because the output of one function is not the input of the other) and we know that they do not change anything about the system (because they are pure functions). The order in which they are executed is completely interchangeable.
The execution order of independent pure functions is reordering and optimizable. Note that if the result of doThing1() is the input to doThing2(), they need to be executed sequentially, but doThing3() can still be reordered before doThing1().
What are the benefits of reordering for us? Concurrency, of course. We can run them separately on three cpus without worrying about any problems.
In most cases, compilers for advanced purely functional languages like Haskell can analyze your code to determine whether or not it can be parallelized, preventing you from shooting yourself in the foot (deadlocks, conditional races, etc.). These compilers could in theory automatically parallelize your code (although none of them currently support it as far as I know, research is ongoing).
Even though your compiler doesn’t work that way, it’s important as a programmer to be able to determine if your code is parallelable based on the function’s signature, and to avoid code that has hidden side effects that can cause threading problems.
conclusion
Hopefully the first task has piqued your interest in FP. Pure, side-effect-free functions make code more readable and are the first step to parallelism.
Before we start implementing parallelism, we need to know something about immutability. We’ll explore this in part 2 of this series and see how pure functions and immutability can help us write easy-to-understand parallel code without resorting to locks and mutexes.