What can we learn from Functional Programming?
The four concepts of Functional Programming. Cover photo created by Ahad Rahman

What can we learn from Functional Programming?

If you've ever researched functional programming, you may find the functional programming community a bit extreme. As in, they will convince you that you need to change the way you write all your code and that you need to?forget everything you know about this programming.

I hate this. I consider these views as idealistic and don't take into account the industry practices and existing tech stacks. I believe these radical perspectives defer programmers from dipping their toes in the world of functional programming because they think they need to change their whole workflow, become a great mathematician, understand lambda calculus, figure out what the heck monads are, and relearn everything.

So rather than this bold statement. I suggest that we?refactor?everything you know about programming. I'm going to go against what most of the functional programming community says and suggest that we don't have to follow every principle that falls under the umbrella term we call 'functional programming', and rather we pick and choose what works for the problem we're trying to solve in the given context. And if you come from an object-oriented programming background, then some of these ideas may seem wild. But if you bear with me, I'm hopeful that you can learn something and apply it to your day-to-day software development.

Disclaimer: This is an opinion piece, and while I have researched functional programming extensively, I do not claim to be an expert. The goal of this article is to encourage outsiders and newcomers to have fun experimenting with functional programming. If you have any disagreements, I would love to have a thoughtful discussion in the comments. The code snippets given in this article were as language agnostic as possible but followed Python syntax closely.

Origins of Functional Programming

Functional programming is a programming paradigm, just like imperative or object-oriented programming. Like all programming paradigms, functional programming is just a different way of breaking up the world and putting it back together through code to model the problem we are trying to solve.

Programming Language Evolution

Diagram showing the evolution of programming languages with examples. From left to right: Assembly/Near machine-code, C & C++/Low-level, Python/High-level
Evolution of programming languages

Computers have evolved significantly since the early days of computing, which has given birth to modern software development to leverage the ever-increasing power of our hardware. None of us writes in Assembly anymore; few of us write in C; most of us use high-level languages like Java and Python (at least in our day job).

A meme describing types of headaches. Ranges from migraines, hypertension, stress, with the most severe being 'Debugging YAML'?.





In fact, some of us don't even write code anymore!






Although our languages are ever-evolving, providing more and more language features, the way we write code has not evolved to the same degree. In some way or another, our programs consist of statements that modify data stored in the computer's memory until that memory contains the desired result. This approach closely resembles how a computer works at the hardware level, so it certainly isn't a bad one, as programs can compile into efficient machine code.

I want you to hold that thought.

An example of a clean UML diagram. There are boxes with arrows pointing towards other boxes without intersection.
An example of a clean UML diagram

Let's say you're developing an application and a rough UML diagram looks something like this. These could represent classes, modules or dependencies. You have a decent design. It works as intended. But time goes on, requirements change, you start fixing bugs, and maybe you also add another module and rearrange some stuff...

An example of a messy UML diagram. There are boxes with arrows pointing towards other boxes with many intersecting lines.
An example of a messy UML diagram

... Until eventually, we get lines and lines everywhere and we start calling "spaghetti code". We know we've reached that stage when we start saying things like, "It works but ...".

  • It works, but it's hard to explain
  • It works, but not when you do this and when that is true
  • It works, but not on weekends and when it's raining and when my manager is testing it

So how do we solve this? Functional programming aims to solve this!

Mathematical Function

Functional programming borrows ideas from mathematicians, who invented functions centuries ago. These functions are different to the functions or methods that you're used to when programming.

Diagram showing each element in an input set getting mapped to an element in an output set. This example is showing the elements of output set are double the elements in the input set.
Diagram showing each element in an input set getting mapped to an element in an output set.


A mathematical function is a relationship between two sets, an input and output set, that maps each element in the input set to an element in the output set. There's no time or other external variables at play. It just exists. The above example would be a function to double a number, as we can see 1 is mapped to 2, 2 is mapped to 4, and so on.

The functions we normally write when programming are just reusable blocks of code. They can do anything from deleting files, updating a database, computing a result from the input, manipulating the input, or even computing return nothing.

Mathematical functions do not behave that way. For a given input, not only does it always produce an output, but it always gives you the same output. Inputting 3 into the function above will always give me 6, whether it's the first time running the function, or the 12th time. The output never changes over time.

Concepts of Functional Programming

The concepts found in functional programming are a direct result of adhering to how functions work in mathematics. The number 1 goal of functional programming is to write code that has no side effects. This leads to code that is:

  • easier to read and write.
  • less prone to bugs.
  • easier to test.
  • more maintainable.
  • easier to scale.

Immutability

The first and likely most important concept of functional programming is immutability. Mutating state is a major cause of code complexity. In functional programming, everything is immutable. This means that once a variable is set, its value cannot be changed. If `x=3` at the beginning of a program, x will always have the value of 3 for the rest of the program.

x = ['a', 'b', 'c']
y = some_function(x)
x = ???        

In the above code snippet, what would the value of `x` be? We don't know for sure, but we can be reasonably confident `x` is an array containing `a, b, c`. We would have to look inside the implementation of `some_function` to be certain.

x = [‘a’, ‘b’, ‘c’]

y = c(a(x)) + b(x) + floor(b(x)) + d(x) + c(d(x)) + c(x) + ceil(d(x)) + a(b(floor(a(x))) + e(x) + f(e(x)

x = ?
        

What about in the above example? We're not so sure anymore that `x=[‘a’, ‘b’, ‘c’]`, as we don't know if `x` has been modified by one of those functions.

In functional programming, everything is immutable so we can be confident that `x` still contains the same elements. How do we get any work done if we can't change any variable? Even the term variable comes from the word 'vary'.

The answer is in copies. If we want to add an element to an array, we get a copy of the array with the appended element, leaving the original array how it is. The goal is to have efficient copying that allows us to manipulate our code, rather than manipulate state. The exact implementation of how this works differs from language to language and is beyond this article. Researching persistent data structures is a great start.

Referential Transparency

The next important concept in functional programming is referential transparency. We say an expression is referentially transparent if we can replace it with its value anywhere in the code.

Referential transparency is not unique to programming. We see it in spoken languages all the time.

For example, these two sentences have the same meaning:

The Big Ben is in the capital of England.
The Big Ben is in London.

We can see "capital of England" is just a placeholder for "London". Using either term gives us the same information.

def square(x):
  return x * x

square(3) == 9        

The same idea applies to code. If you can replace the call of a function with its actual value, then the function is referentially transparent, as shown in the above snippet.

I find it easier to look at examples of functions that are not referentially transparent, also known as 'referentially opaque'.

1.

def get_todays_date():
? return date.today()        

Finding today's date may return February 20th today, but if we replace the function call with that value, then we will be computing the wrong result on different days. Therefore, this function is not referentially transparent.

2.

num_points = 42
def increment_points(x):
? return num_points += x        

Functions that modify and return global values are not referentially transparent. We cannot guarantee the function will always return the same output given the same input as other functions may modify global values.

3. Functions that rely on files are usually not referentially transparent. Even if we provide the same file path to this function, we cannot guarantee the file has not been modified, and thus, cannot guarantee the same output.

def read_file_contents(file):
? with open(file) as f:
  ? return f.read()        

We want to minimise functions like the three above examples, and instead, maximise referentially transparent code. Doing so will give us a much stronger expectation that our functions have no side effects.

Higher Order Functions

The next concept in functional programming is a bit more abstract as we look into higher order functions. We want to be able to treat functions as their own entities. The idea is to treat functions as values and pass functions as if they were data.

A higher order function is a function that takes another function as an argument or returns a function. You may have unknowingly used them through functions like map, reduce or filter, especially if you have used JavaScript or Java's streams. The aforementioned functions use another function as an input.

Higher order functions allow us to stop writing functions like this:

def square_list(nums)
? squared = []
? for n in nums:
  ? squared.append(n * n)
  return squared:        

This is extremely imperative. This is how computers think, not how humans imagine squaring a list of numbers. It's a lot of unnecessary code when the main logic is `n*n`.

Instead, we want to maximise our reusability of existing functions, especially standard libraries as they are tried and tested. A more functional programming appropriate way of writing the same function is:

def square_list(nums)
? return map(lambda n: n * n, nums):        

The above allows us to focus on the main logic of the function by using a lambda, which is acting as an anonymous function. If you are unfamiliar with lambda's, an alternative way of writing the above function is:

def square(n):
? return n * n

def square_list(nums):
? return map(square, nums):        

Higher order functions also allow us to minimise duplicate code. Notice the amount of duplicate code in the two examples below:

1.

def short_words(words):
? filtered = []
? for i in range(len(words)):
??? word = words[i]
????if len(word) < 3:
  ????filtered.append(word)
  return filtered        

2.

def starts_with_s(words):
? filtered = []
? for i in range(len(words)):
??? word = words[i]
????if word.startswith(‘s’):
????? filtered.append(word)
? return filtered        

Both functions iterate over a list, filter on a given condition, and add to a placeholder list which we return. The only part that differentiates these two functions is just a one-liner on the condition.

A more functional programming-centric way of writing these two functions would be as follows:

1.

short_words = filter(lambda w: len(w) < 3, words)        

2.

starts_with_s = filter(lambda w: w.startswith(‘s’), words)?        

We can see the filter function has done the abstraction work for us. The lambda function is passed to the filter function as an input.

Higher order functions allow us to write code that minimised side effects as the functions are treated as values or data.

Lazy Evaluation

I don't like the name of this last concept of functional programming because it has a negative connotation to it. Lazy evaluation simply means that an expression is not evaluated until its value is needed, which is the opposite of eager or strict evaluation.

Haskell is a functional programming language whose fundamentals are based on lazy evaluation.

I find learning by example the best way to digest a concept. Consider the following:

def expensive_operation(num)
? print(“Sleep being called”)
? sleep(3) # Sleep for 3 seconds
? return num + 1

if condition:
? x = expensive_operation(42)
? print(x)
else:
? x = expensive_operation(42)
? print(“Hello!”):        

In most programming languages, including Python, you would see the following if `condition` was true.

  1. "Sleep being called" printed.
  2. After 3 seconds, "43" would be printed.

If the `condition` was false, you would see nearly the same thing

  1. "Sleep being called" printed.
  2. After 3 seconds, "Hello!" would be printed.

This is known as eager evaluation, which many programming languages and their compilers follow. Each line is executed sequentially and the result is stored in memory. Eager evaluation is great with object-oriented programming since different objects can update the state of different variables at different times. Therefore, executing each line as you encounter them is a must if you want the true value at that point in your code.

However, in this example, we can see that the 'else' branch does not use the value of `x`, so we lose some performance and we execute the expensive operation.

Lazy evaluation does not execute `x = expensive_operation(42)` until the value is needed. It is called "lazy" because, like a lazy student procrastinating on their assignment, it will only do the task until it is actually needed.

When the compiler reaches that statement, it will add a flag to remember to compute the result when the value is required.

Going back to the above example, if the condition was true, x will be computed at the point of printing. If the condition was false, then x is never computed.

You might be thinking "Wow! Lazy evaluation offers a lot of performance gains, this is great!". You're partially right. Lazy evaluation only works because of referential transparency, as we can guarantee that we get the same output given the same input.

Without referential transparency, we cannot defer the `expensive_operation` calculation until later because we don't know if the value will be different.

The Intersection of the Concepts of Functional Programming

Showing how functional programming lies in between the four concepts of functional programming.

Together, at the intersection of these four main concepts lies functional programming. They all contribute to the overall goal of functional programming to have no side effects, consequently?leading to code that is:

  • easier to read and write.
  • less prone to bugs.
  • easier to test.
  • more maintainable.
  • easier to scale.

Functional Programming Checklist

Personally, functional programming concepts can be quite abstract and I find it difficult to determine if I'm writing good, functional code. So I've broken them down for you and composed a checklist that I try to follow in my day job.

  1. The return value of a function does not vary according to anything that is not supplied as a parameter.
  2. The parameters are not altered in the function.
  3. Nothing outside the scope of a function is altered.
  4. The result of a function will always be the same when supplied with the same parameters.
  5. Using existing standard functions when appropriate.
  6. Each function is understandable on its own.
  7. Reusing functions when sensible.
  8. Big computations are built out of smaller ones.

The first four are trivial to check on a per-function basis. The next four are a bit more general, and cannot be verified as easily as they require developer judgement.

Closing thoughts

The goal of this article was not to convince you to switch your entire tech stack to only use functional programming languages, or to only write functional code. I’m not saying change every for loop you have in your code to a map function.

But I do believe the advantages of functional programming can be applied to other programming paradigms, and by roughly following the checklist I provided before, we can write higher quality code. I really hope you dip your toes into the realm of functional programming. You will be forced to think outside the box which reignites that euphoric feeling of getting the computer to do what you want it to do that we all felt when we first started programming.

Jericho Jackson

Systems Engineer | Atlassian

2 年

Good read, nice to see functional programming broken down in an easy to digest way for once

Nam (Kevin) Hai-Le

Master of Communication at Te Herenga Waka - Victoria University of Wellington | Ex VMware by Broadcom

2 年

my inspo :)

Dominic Tjiptono

Specialist I Developer at IAG New Zealand || 93 LinkedIn Skill Badges

2 年

Wonderful and useful article Ahad Rahman

要查看或添加评论,请登录

社区洞察

其他会员也浏览了