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.

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.

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.

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.