Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
This quick and pacy functional programming overview will show you a quick glimpse of the main areas of the functional programming world. Writing your code the functional way might help you organise your code better, and control complexity in ways that have stood the test of time. If you are ready to sacrifice some performance at the altar of more expressive and easy to maintain code, functional programming will equip you with knowledge you need.
Declarative and Imperative
You can write your programs in two different ways. Either by writing code that talks about what you want to do or by writing code that gives step by step instructions for what you want.
For example, here is a declarative statement : "Natural logarithm of a number X is a number y for which e**y = x
". This tells you what a natural logarithm is, not how to find one. (Part of the reason why logarithms feel so mysterious in school, because you don't know how to find one, although you know what it is).
Here is an example of an imperative piece of code, which finds the hypotenuse of a triangle with sides a and b.
const aSquared = a**2;
const bSquared = b**2;
const aPlusBSquared = aSquared + bSquared;
const hypot = aPlusBSquared ** 0.5;
It's rather rare to see someone code in a chunked and piecemeal manner shown above, most people would say :
const hypot = (a**2 + b**2)**0.5;
Rather than showing you the steps for calculating the hypotenuse (the computer can figure out whether to add or square first etc.), it just says "The hypotenuse is the root of the sum of squares of two sides"). Looks more concise ? I think so too.
It is a dream of programmers to be able to write code that is 100% declarative and very efficient everywhere - a dream that we are still working towards. Generally, declarativeness can cause efficiency problems, like in the code below :
const fib = (n)=> n<=1 ? 1 : fib(n-1) + fib(n-2);
The above one-liner finds the n-th fibonacci number for you, and it looks beautiful to me. It's literally the definition of fibonacci numbers put into code which says the first 2 fibonaccis are 1, other fibonacci numbers are the sum of the previous two fibonacci numbers
. Very declarative. But someone who has done the asymptotic analysis (no you don't have to know what it is to understand this article) of the function knows it is just as horrible in efficiency as it is beautiful in expressiveness. A programmer aspires to be a writer, but the practical concerns of efficiency shatters the dream - not the first group of people at all to go through this plight.
Functional programming aspires to the same dream, and while it does so by sacrificing on efficiency a little bit, we have come far in making our code look beautiful while still running at a reasonably good speed. In short, functional programming is declarative.
Some comparisons
I'm just going to write some code, first in the functional way, and then in the declarative way.
// functional
const sum = (a,b)=>a+b;
console.log(range(1,100).reduce(sum)); // 1 + 2 + 3 ... + 99
// declarative
let sum = 0;
for(let i=1;i<100;i++){
sum+=i;
}
console.log(sum);
The first piece of code says, "sum is where you add two numbers together. Now, take the range of numbers from 1 to 100, add them and console.log". The second one is tedious in the stepwise way it does things. It is read literally - the sum is initially zero, now iterate over each number from 1 to 100 (even the 1 to 100 part is not clearly expressed, it's implicit in the for-loop), add them to sum. Now console.log the sum.
The difference in the amount of readability between functional and imperative code can grow rather large, if there are lots of parts moving around in your code.
Pure functions and immutability
No imperative code means that you cannot have your functions modify a variable. This is imperative.
let x = 4;
function addFive(){
x+=5;
}
Just use const
s and you'll catch yourself if you start to write imperative code, because the interpreter will throw an error.
This below is declarative. Or is it ?
const x = 4;
function addFive(){
return x + 5;
}
This is definitely much better than before, as addFive is no longer modifying x itself, and that is first step towards writing declarative code. It's not possible to write declarative code if you are modifying your values rather than creating new different values, because modifying always means you have to assign first, which makes it stepwise (and thus imperative). When functions don't modify existing values but create new ones, they are called pure. Writing pure functions allows to you eventually build this more declarative version :
const x = 4;
function addFive(x){
return x + 5;
}
This applies not just to functions. In truly functional programming, arrays of the traditional kind are not allowed, because typically you modify an array over the course of the program.
const a = [];
a.push(4); // not allowed
a[0] = 9; // also not allowed
Therefore, data structures of an immutable kind (ones which cannot change once created) are necessary. Where you would have modified existing data in an imperative program, you create new data in functional programming. This causes efficiency problems, as well as consumes more memory when you are creating new data for every modification, but allows you to do more without complicating your program in large programs.
Closures
Functions can remember their enclosing scope, even long after they have been returned from it and live in a different environment. For example :
function doWith(x, y){
return function(){
y(x);
}
}
const sqrtTwo = doWith(2, Math.sqrt);
sqrtTwo(); // -> 1.41....
Above, sqrtTwo remembers the two arguments 2, and Math.sqrt it was called with, even though they were passed only to the parent function doWith, and not saved explicitly. In fancy language, we say "sqrtTwo has closure over x and y". The concept can be summarised as a returned function remembering the values from the returning function's scope even after the returning function has finished execution.
First class functions
In functional programming, functions are just like other values (it's a compliment). It means that we can pass functions to other functons as well as return functions from other functions. We do this extensively when we use map, which takes a function as an argument, as well as in filter and reduce. First class functions form the crux of functional programming and make it possible for us to combine functions in extremely interesting ways. Infact, many of the tricks mentioned here wouldn't have been possible if functions weren't first class citizens in a functional programming langugage.
Recursion
Recursion isn't exclusive to functional programming, and if you have had any decent programming exposure, you probably have come face-to-face with recursion. Recursion is when something is expressed in terms of itself.
For example, we can define natural numbers in terms of themselves : A natural number is either 0 or the successor of a natural number. As long as successor is well-defined, this allows you to build up natural numbers from just 0 and a successor function - it is not a fallacy for a definition of a term to contain itself, rather, a lot of beautiful things come out of it.
Currying
Currying allows you to partially apply arguments to a function and leave the rest for later. For example, say you have a function pow, which raises number a to power b. You can curry it to apply some power x to b.
function curryPow(pow, exp){
return function(x){
pow(x, exp);
}
}
const square = curryPow(pow, 2);
const cube = curryPow(pow, 3);
square(4); // -> 16
cube(4); // -> 64
Here is a general currying procedure :
function curry(f, ...args){ // first argument : f. The rest are stored in args[]
return function(...rest){ // return a function which takes some arguments rest[]
return f(...args, ...rest); // and apply to f, the arguments which were supplied
// earlier in args[] and the arguments rest[]
}
}
Looks rather abstract. Even though I have supplied the comments, I see that the ...
operator might be causing problems here, because it means two things. In the function arguments list, ...
means "the rest of the arguments as an array", while in other places in the code, it means "spread/open this array". Interesting thing is that you can use it to curry multiple times too.
Here's an example. I've assumed here that I have a range function similar to python's written already:
const fromOne = curry(range, 1)
const oneToHundred = curry(fromOne, 100)
const everyOtherFromOneToHundred = oneToHundred(2); // -> [1,3,5...99]
Pipelining
Typically, you apply a series of functions to a value when you're doing functional programming. For example, the code below finds sums of primes till 1000.
range(1,1000).filter(isPrime).reduce(sum)
We can instead pipeline these functions into a single function.
function pipe(...functions){
return function(...args){
return functions.reduce((result, f)=>{
return f(...result);
}, args);
}
}
The pipe function takes a series of functions, and returns a function which will execute the given series of functions on its arguments one by one, and return the result. That's a mouthful, but the functional programming world is often full of such mind twisting sentences which feel enlightening once you get them.
Without spending much time on how the function works just yet, let's use it in an example :
const squareElements = (array)=>array.map(x=>x**2)
const hypotenuse = pipe(squareElements, sum, Math.sqrt);
hypotenuse([3,4]); // -> 5
I had to create a squareElements function prior to piping it, because otherwise I would have to write a separate currying function that curried from rightwards. If we had such a function, we could just write :
const hypotenuse = pipe(reverseCurry(Array.prototype.map.apply, x=>x**2), sum, Math.sqrt);
The line above uses reverseCurry to say "Take array.map without an array, where the function squares each element. Return a function which will take the array". It is equivalent to our squareElements function above.
That's it ?
There is more to functional programming than the concepts covered above. However, what has been written above, when applied to its fullest power, gives you a lot of power and forms most of your functional code. Additional concepts like functors and monads not covered in this article can provide more fuel for the functional programmer's gas tank.