Functional Programming: Concepts, Idioms and Philosophy
Functional programming is proposed as a solution to most modern problems, such as concurrency and scalability. To some, it’s an arcane concept that applies only to Erlang, Haskell, and a few other strange languages that are either too complex or irrelevant. This simply isn’t true, so I’ll show you how to apply some functional programming to non-functional languages.
I’ll begin by defining what “functional programming” really means, then deconstruct the functional paradigm by explaining common idioms and comparing syntax. Finally, I’ll show you how to make relevant changes to non-functional ones to follow the philosophy of functional programming.
Please note that this article is mainly for people who never had a functional programming before, and the goal here is to functional programming as a kind of practice for rendering, not only on language features, also as a concept, the concept, to a certain extent, can follow in any language, improve the safety of the code, And brings some of the benefits of functional programming to non-functional languages.
Define ‘functional programming’
The simplest definition of functional programming is pure functions (or, more simply, deterministic functions).
On the other hand, this can have many meanings and must be carefully analyzed. This leads to some other important rules of functional programming, such as immutable variables and combinatorial functions.
As a general rule, think of your code as if you were writing a mathematical function:
- Should its results be different depending on something that is not a parameter? not
- Should it change any of the application’s parameters? not
- Should it change things beyond its scope? not
- Should the results always be the same for parameters in the same application? Is dripping.
When you read this, if you think of a code that doesn’t fit, then I’m afraid it’s not functional. Why is that?
Functions are so special in those languages that they become first-class citizens. They can be passed as “variables” that can be partially used to compose new functions. I’ll talk more about this when I describe some idioms later. The important thing to remember here is that functions are designed to be reusable and composable. Any side effects or external distractions can make the function unpredictable and difficult to reuse.
Immutable values
State systems are very difficult to parallelize and must implement mutexes, locks, semaphores, and other forms of access restrictions to make code more secure. Functional programming simply strikes at the concept of variability; In contrast, when coding in a functional language, you write functions to get the values you want.
This is one of the most difficult concepts to describe, coming from a non-functional language. The advantage of this is that immutability forces you to rethink your problem to have a correct solution to that problem. Once you understand that functional programming is more about how you design your functionality than how you design your data, this shouldn’t be a problem.
After all, values are logical abstractions of your data and functions are logical abstractions of your business.
Moving from object-oriented to functional programming can be complicated if you’ve never seen the source code and what it looks like. You probably won’t see the for and if commands you’ve used before, but instead map, reduce, filter, and flatten. You’ll learn about (less complicated) monads, functors, and algebraic data types. What should all this mean, and why can’t you use the old structure?
I’ll use scala names for the idioms presented here, but they are the same in functional languages.
Monad, Functor and Friend
Monad is very simple, with very complex rules. Monad is the container. There are rules that govern how Monad works and interacts with other MonAds. These rules also define terms for people who have never programmed in a functional language, such as Functor, Monoid, Applicative Functor, and a few additional terms. For simplicity’s sake, I’ll group them into simple ‘Monad’ here, at the risk of semantic error to get you to understand the concept. I promise I’ll disambiguate it later, okay?
Let me show you some Scala code to illustrate what this means:
def someComputation(): Option[String] = .
val myPossibleString = someComputation()Copy the code
For example, imagine that you could have a value (in this case, a String) as the output of some computation, or none at all. Instead of null, which can cause serious problems, such as not-so-good NullPointerException, you can return Option[String]. Option is a Monad provided by Scala that wraps your data and allows you to interact with it safely only when it exists.
In Java code, you check for NULL before doing anything to avoid NPE (NullPointerException) :
String myPossibleString = someComputation();
if(myPossibleString = = null) {
//Short circuit out
}
return myPossibleString.toUpper();Copy the code
In functional programming languages, you can use Map on Monad to achieve the same level of security:
myPossibleString.map(_.toUpper)Copy the code
The only difference here is that Java code returns a String, whereas Scala code returns an Option[String].
By mapping a monad, we manage to translate the underlying values while maintaining the same vessel. The result can be:
Some("MYUPPERSTRING") // if the computation was successful
//or
None // if the computation was unsuccessfulCopy the code
Note that _. ToUpper does not break if applied to None. This allows linking operations on a value without short-circuiting all possible problems:
String myStr = someComputation();
if(myStr = = null) {
return false;
}
myStr = newOperationOnStr(str);
if(myStr = = null || myStr.length < 3) {
return false;
} else if (!matchRegex(str)) {
return false;
} else {
return true;
}Copy the code
This code can be written in Scala using Option[String] :
someComputation()
.map(newOperationOnStr(_))
.filter(s => s.length > = 3 && matchRegex(s))
.isDefinedCopy the code
Here, I could spend hours writing about how Monad allows you to better express your code in a functional way, but I’ll leave it up to you to decide.
There’s a whole mathematical theory behind books like this, but I’ll modestly limit myself to explaining that algebraic data types are meaningful compound types. An algebraic data type is defined by the sum of all definitions of the form _. Some monads are defined as algebraic data types, such as the Scala Options we saw earlier, which can be Some or None. They each behave differently when mapping, filtering, and reducing.
This means that while MONAD provides rules for data containers, algebraic data types provide form and meaning. Try and Option are similar in terms of their APIS and can behave very similarly, but the difference is that you want to use Try when the error might be meaningful or there are multiple types of errors that can be thrown, each of which requires a different operation. You can also use Either if you have to deal with two possible outcomes for a function. The range of unary algebra types is huge, and they help you build applications without the necessary concepts of heavyweight.
In general, using this type abstraction results in cleaner and more meaningful code. Personally, I find this abstraction more advanced and valuable than object-oriented programming. Of course object-oriented programming has its value, but it is easier to express logical algebraic data structures.
But how do I start using it?
What makes a code truly functional is the usage of aforementioned concepts. The same way you can have OO code in scala, you can have functional code in python, for example. While some languages provide native idioms that let you write code that corresponds to functional concepts, there are several concepts that can be ported to non-functional (or native non-functional) languages. It is important to note that writing code in a functional language does not immediately make it functional. What makes a code truly functional is the use of the above concepts. For example, using the same approach, you can use object-oriented code in Scala, and you can write functional code in Python.
Imagine a situation like this, replacing a dummy function with something real:
my_value = []
def fetch_values() :# Imagine that you're fetching a real data
# I'll return a dummy list of dummy ints
my_value = [8.3.2.5.1.4]
def filter_values(even=True):
t = []
for i in my_value:
if even:
if i % 2 = = 0:
t.append(i)
else:
if i % 2 = = 1:
t.append(i)
my_value = t
def process_value(x) :# Also dummy here.
return x * x
def process_all_values() :for i in my_value:
process_value(x)
def do_process():
fetch_values()
# We (for some reason) need to process evens before odds
filter_values(true)
process_all_values()
fetch_values()
filter_values(false)
process_all_values()Copy the code
I broke every possible rule to make the example easier. Hopefully, you recognize some of the above anti-patterns in real production code. We’re going to get rid of them. Next, I’ll write Python code to address all of the above anti-patterns, but try to find them and imagine a functional approach before actually reading the transformed version.
from functools import reduce
def fetch_values() :return [8.3.2.5.1.4]
def partition_values(vals) :return reduce(lambda l.v: l[v % 2].append(v) or l, vals, ([], []))
def process_value(x) :# Processing here is a pure function.
return x * x
def process_all_values(lst) :# Be cautious when plumbing functions here;
# You should only map over pure functions, to avoid
# intermittent state or unhandled errors.
return map(process_value, lst)
def do_process():
id_list = fetch_values()
evens, odds = partition_values(id_list)
p_evens, p_odds = process_all_values(even), process_all_values(odds)
# Python `map` is lazy, so we force evaluation
list(p_even)
list(p_odds)Copy the code
Although not optimal due to Python’s limitations, we have a more functional code. There are a number of enhancements we can make here, such as wrapping our mapping calculation with unary algebraic types, allowing us to safely handle errors that might occur during process_value.
Also, note that although the code snippet is functional, it is only safe to do so if process_value is a pure function. Otherwise there are problems, such as an indeterminate state if an exception is thrown in the process, or you have to reprocess everything.
Other concepts to help you deal with this kind of problem and are orthogonal to functional programming, such as idempotence. It’s important to know that functional programming (or the idea of functional programming) is just a tool that can help you write safer code.
You’ll find that code that follows the functional philosophy is easier to test, parallelize, reuse, and understand. This certainly does not mean that functional programming is a panacea for all programming problems. If that were the case, I would not advertise the practical use of non-functional languages, but rather advocate porting to functional programming languages. The real value here is that you have an alternative to the traditional imperative code style, which may provide the benefits described above.
Although this article is longer than I originally thought, it has only scratched the surface of functional programming. In future articles, I’ll definitely be writing more about functional programming, and I’ll always be more focused on ideas and how you can think functionally, even if the language you’re currently using doesn’t fully implement functional idioms. There are a number of useful techniques that can be borrowed from functional programming to make code safer, cleaner, and more expressive.
If you agree, disagree, or just want to have a drink with me, feel free to write me something. My blog still doesn’t have comments, but next week, I’ll tackle this issue: X