When I first saw the ideas of functional programming, I found them very strange due to the fact that, I, like most people, got used to structural and object oriented programming paradigms. The structural takes away the goto
definitions from our code and replaces them with if/else/do/while
, which forces the code to execute in an order. The object oriented, encapsulates local variables and methods long after a function returns (what eventually became a constructor) and through the use of function pointers introduced polymorphism. In this post, I will introduce you to functional programming.
Functional Programming in a Nutshell
It is the direct result of Alonso Church, who invented Lambda Calculus in 1936. FP treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
Before I jump to the characteristics, I want to point out that the terms highlighted
will be explained later on. All code examples will be written in Javascript.
The Characteristics of Functional Programming
- Immutability - Once a variable is created, it cannot be mutated.
- Declerative - Describes what the program must accomplish in terms of the problem domain, rather than describe how to accomplish it as a sequence of the programming language primitives.
- Higher-order functions and functions as first class citizens - Functions can take other functions as arguments or return them as results. With the help of
closures
, functions allowcurrying
andpartial application
. - Pure functions - functions that have no
side effects
. - Recursion - For loops inherently mutate state, remember incrementing i ? As FP does not allow mutating state everything is done with recursion. Do not worry about stackoverflow errors, as FP has optimized recursions called
tail call recursions
.
We saw some characteristics, now, lets see what are closures, currying, partial application, side effects and tail call recursions.
Closures
Think of a function’s variables as a bag. When the function is returned (removed from the stack and its frame is destroyed), its bag remains. We previously said that functions can return other functions and closures come into play when an outer function is destroyed and the inner function wants to access previously passed arguments (the outer functions bag).
1 | function firstName(firstName){ |
You see how we stored "Will"
in the “bag” of the firstName function. Even after it returned, we still had access to its variables. It allowed us to reuse the name Will and call it with different last names.
Currying
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument.
Lets see an example without currying
1 | function entireName(firstName, middleName, lastName){ |
With function currying, if we pass 1 argument, we will recieve a function back and only when all arguments are passed, our function will execute.
1 | function entireName(firstName, middleName, lastName){ |
Partial Application
Partial application works similar to currying. The difference between them is that currying splits a function to functions that receive one argument, also called unary, while partial application allows passing multiple arguments. Lets see an example
1 | function entireName(firstName, middleName, lastName){ |
Side Effects
In programming, a side effect is when a procedure changes a variable from outside its scope.
For example:
1 | function updateVariable(){ |
We changed some state outside of our function - this is called a side effect.
Functional programming is against side effects, but without side effects we would not be able to write software. Instead we try to limit the amount of side effects, allowing only a portion of our code to carefully do them.
Recursion
Recursion in computer science is a method of solving a problem where the solution depends on solutions to smaller instances of the same problem (as opposed to iteration). Basically, a function calls itself multiple times until some condition is met.
For example, lets see a fibonacci recursion, do not mind the big O notation:
1 | function fibonacci(num) { |
This function will call itself repeatedly until the condition above is met and until it does that, the function will create multiple frames of itself, possibly leading to stackoverflow.
To address the recursion problem, tail-call optimization is used. Tail-call optimization is where you are able to avoid allocating a new stack frame for a function because the calling function will simply return the value that it gets from the called function. Usually, the compiler takes care of tail call optimization in functional languages, I will not elaborate on when tail call optimization does not occur, but if you get into FP in more detail, make sure to read about it.
Bonus: Pipes
Lets see how we put to practice all the above. In functional languages we usually have a pipe operator that allows an argument to pass through a series of functions.
1 | /* |
If you are familiar with UNIX style programming, FP looks similar.
1 | ls -l | uniq | wc -l |
If you did the above in a terminal before, you have used functional programming! Each of the functions above does one thing without mutating side effects and with the use of the pipe(|) operator we are able to combine our functions to achieve our end goal.
What functional programming helps achieve
All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables. Instead of wiring a solution to these problems, we avoid them altogether ahead of time using functional programming!
Summary
We saw what is the definition of functional programming. Later, we saw the basic characteristics of it, like currying, partial application, immutability, higher order functions and recursion. Afterwards, we looked at a few examples and learned how to leverage FP to our advantage without mutating state. In the end, we saw how pipes in functional programming help us achieve readability and expressiveness.