What’s the point of Functional Programming?

Yang Hsing Lin
Level Up Coding
Published in
6 min readMay 6, 2020

--

Jazz Tree, beautiful but irrelevant photo. Photo Credit: https://unsplash.com/photos/GfAqbDrCoio

Recently I read the book “Functional Thinking” by Neal Ford. I found it interesting and decided to sort out my thoughts in this post. This post is by no means a book review nor a TLDR. Most of the contents are based on my own experiences and understanding.

About Me

I’m “trained” with imperative programming and Object-Oriented style. But as a programmer feeling at home with Ruby and JavaScript, I am kind of familiar with the “elements” of functional programming. I know “function as a first-class citizen,” curry, compose, declarative programming, and how they work in real-world battlefields such as React and Redux. I have a few experiences with Elixir and know how to replace loops with recursive functions. But still, I feel something is missing: A whole picture of the paradigm connecting all the dots. And I started to read the book.

Before starting reading, I have several questions in mind:

  1. What’s the point? What’re the advantages of all the mind-twisting?
  2. How are the “elements” related to Functional Programming?

After reading the book, I think I can answer the questions somehow.

Super Brief Functional Programming Overview

Let’s set the scene first by having a super brief and informal overview of Functional Programming. Functional programming is a paradigm where functions are first-class citizens, which means, in plain words, they can appear everywhere one can think of. Loops are replaced with abstractions such as “map,” “reduce,” and “filter.” One can create functions by currying, composing, optionally leveraging closure. Most of the time, variables are immutable, meaning they can not be modified once created. Exception or stack jumping is unusual and has different meanings than their imperative parallel.

What’s the point?

It’s all about Abstraction.

Functional Programming abstracts things in a different way than Imperative Programming. It abstracts some “operations” from the Imperative one, such as loop, map, and reduce. With the usually-tedious operations being abstracted, developers can focus on higher-level logic and make the code more expressive.

To illustrate that, I’ll use the following code snippets of JavaScript. Although not being a “qualified” functional language for many people, it’s enough to demonstrate the idea here. Suppose we have a list of numbers, and we want to find the even ones and double them. (Don’t ask me why the world does anyone want to do that.)

For the imperative approach:

const myNumberArray = findSomeNumberArray()
const evenNums = []
const doubled = []
for (let i = 0; i < myNumberArray.length; i += 1) {
if (isEven(num)) {
evenNums.push(myNumberArray[i])
}
}
for (let i = 0; i < evenNums.length; i += 1) {
doubled.push(double(evenNums[i]))
}

For the functional approach:

const myNumberArray = findSomeNumberArray()function isEven (num) {
return num % 2 === 0
}
function double (num) {
return num * 2
}
myNumberArray.filter(isEven)
.map(double)

As we can see, the latter approach is more expressive. One can know what the code is trying to do at first glance (though the purpose behind it is still mysterious because there is no one.) Also, by shifting the place we abstract, we change not only the granularity of code reusing but the abstraction layer on which we think about the problem. Now the `isEven` and `double` can be shared elsewhere, and we can focus on “what” we want to do instead of “how” we do it.

Another benefit brought by the abstraction is it opens up “seams” for the runtime. When the Garbage Collector was first invented, the lives of developers changed at that point. No longer needing to manage the memory, developers could focus on more the business logic and less the system-level communication. Parallel to that, by abstracting the “operations” like above, Functional Languages open up “seams” for the runtime to take over some potentially tedious works such as concurrency handling and state managing (the moving parts.)

OO makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts. — Michael Feathers (Quote from the Book)

Some examples of Functional languages minimizing the moving parts are

  • combining the concurrency control with data structures
  • minimizing state by immutability and pure function

Again, being free from the “moving parts”(threading, state managing) developers can focus more on business logic and expressiveness.

Of course, nothing comes without costs. For the language runtime to handle those tasks well, developers must write the code or even think about the problems in specific ways. Just like you organize your code differently in C++ than in Java due to the presence of GC, usually, we need to structure our logic differently in functional languages than in imperative ones.

In my opinion, these responsibilities shiftings are less right or wrong than trade-offs. The job for us, the developers, is to find the right tool for the right task.

How are the “elements” related to Functional Programming?

I’ve often heard about “elements” of Functional Programming such as Immutable, different ways of handling exceptions, recursion. I understand each of them, respectively. But my question is: How are they related to Functional Programming?

Immutable

Immutable data can not be modified once initiated. The implication is that you can be sure the “value” is the same if you hold the same reference. For the most functional mechanisms to take effect, it requires the functions passed around to be pure: A side-effect-free function whose returned value is solely determined by its input arguments. That’s why so many functional languages come with immutable data: Only when the data is immutable can the functions be pure.

Error Handling

The most common way of handling errors is Exception: You throw an exception, and the execution flow jumps to wherever the line of code that catches it.

function sthMightGoWrong (a, b) {
if (everythingGoesWell()) {
return 'the-result'
} else {
throw new Error('ops!')
}
}
try {
const result = sthMightGoWrong('hello', 'world')
} catch (e) {
// execution jumps to here
}

Another way of handling errors is by returning values. Such a mechanism is used by some imperative languages such as Go and most functional ones like Elixir.

function sthMightGoWrong (a, b) {
if (everythingGoesWell()) {
return { ok: true, result: 'the-result' }
} else {
return { ok: false, return: null }
}
}
const { ok, result } = sthMightGoWrong('hello', 'world')
if (!ok) {
// error handling without stack jumping here
}

The latter is related to functional programming in that stack jumping is a side effect. A function is no longer pure if it may throw: Given the same set of input arguments, there are more than one results: returning some values or jumping to somewhere else. That’s why the traditional throw/catch error handling is uncommon in functional languages.

Recursion

Recursion is a technique we divide-and-conquer a problem by invoking a function itself (this is a super sloppy definition. See the official one here.) It is an approach that can solve tricky problems that are usually non-trivial otherwise. In functional programming, it’s often used to replace looping control flows in imperative languages such as `for` and `while`. But here again, why?

To say “Hi” for multiple times by loop, for example:

// say hi by loops
function sayHi () { console.log('hi') }
function sayHiForTimes (times) {
for (var i = 0; i < times; i += 1) {
sayHi()
}
}

To do it recursively:

function sayHi () { console.log('hi') }
function sayHiForTimes (times) {
if (times === 0) {
return
}
sayHi()
sayHiForTimes(times - 1)
}

The answer is similar to the above: For the benefits brought by the abstractions to work, there must be no shared state. For example, if we share any state, it becomes hard, if not impossible, for the runtime to handle concurrency for us.

The loops use shared states to control the flow by nature. That’s why we use recursion, which shifts the “state” into subsequent function calls.

Afterthoughts

To me, the thing that amazed me the most is the implication and possibilities brought by the different ways of abstraction. Sometimes, only after the lowest-level things are abstracted away can we start to see the bigger picture on the upper layers. It’s similar to the compound interest in that the benefit gained by the current round can speed up the next round.

Also, the paradigm-shifting has us to deconstruct a problem in a different way, which is always an enjoyable thing. 💖

Thanks for the reading. Please leave your comments below if there’s anything you want to discuss.

--

--

Love coding, making sense of things, solving complex problems with elegant solutions.