Functional Programming

Functional Programming

Functional programming is a programming paradigm in which we try to solve everything in a pure mathematical functions style. It is a declarative type of programming style. Its main focus is on “what to solve” in contrast to an imperative style where the main focus is “how to solve”. You can read more about declarative vs imperative programming here.

Programming Languages that support functional programming: Haskell, JavaScript, Python, Scala, Erlang, Lisp, ML, Clojure, OCaml, etc.

History

Functional Programming is based on Lambda Calculus. Lambda calculus is a framework developed by Alonzo Church to study computations with functions. It can be called the smallest programming language in the world. It defines what is computable. Anything that can be computed by lambda calculus is computable. It is equivalent to the Turing machine in its ability to compute. It provides a theoretical framework for describing functions and their evaluation and forms the basis of almost all current functional programming languages.

Basic Concepts

Pure functions

These functions have two main properties.

  1. They always produce the same output for the same arguments irrespective of anything else, i.e. They are deterministic.

  2. They have no side effects i.e. they do not modify any arguments, local/global variables, or input/output streams. This property is called immutability. The pure function’s only result is the value it returns.

Example:

function sum(x,y) {
    return x+y;
}

Recursive functions

Functional languages have no “for” or “while” loops. Iteration in functional languages is implemented through recursion. Recursive functions repeatedly call themselves, until it reaches the base case.

Example:

function fib(n) {
 if (n <= 1) return 1; 
 else return fib(n - 1) + fib(n - 2);
}

Referential transparency

In functional programs variables once defined do not change their value throughout the program. Functional programs do not have assignment statements. If we have to store some value, we define new variables instead. This eliminates any chances of side effects because any variable can be replaced with its actual value at any point of execution. The state of any variable is constant at any instant.

Functions are First-Class and can be Higher-Order

First-class functions are treated as first-class variables. The first class variables can be passed to functions as parameters, can be returned from functions, or stored in data structures.

Higher-order functions are the functions that take other functions as arguments and they can also return functions.

Example:

function hello() {
    console.log("Hello world");
}

function compute(a) {   // compute is a higher order function
    return function(b) { // return a function
        return a+compute(b);
    }
}

function display(f) {  
    f();
}

display(hello) // function hello() as a first class variable
// here display function is also a higher order function

Principles of functional programming

  1. Don’t mutate data.

  2. Use pure functions: fixed output for fixed inputs, and no side effects.

  3. Use expressions and declarations

When we satisfy these conditions, we can say our code is functional.

Additional concepts

Closures

A closure is a function that has access to the variables in its outer function and surrounding state (lexical environment). This means that a closure allows an inner function to access the scope of an outer function.

Example:

function func() {
    const name="Hello";
    function display() {
        console.log(name);
    }
/* 
display function when called anywhere will have reference to their
lexical scope and always print "Hello". display forms a closure over
func();
*/
}

Currying

Currying means breaking down a function that takes multiple arguments into one or multiple levels of higher-order functions.

Example:

const add = a => {
    return b => {
        return a + b;
     };
}; 

add(3)(4); // 7

The benefit of currying is memoization. We can now memoize certain arguments in a function call so they can be reused later without duplication and re-computation.

Example:

// assume getOffsetNumer() call is expensive
const addOffset = add(getOffsetNumber()); 
addOffset(4);     
// 4 + getOffsetNumber()

It is better than calling the same function multiple times.

add(4, getOffsetNumber()); 
add(6, getOffsetNumber()); 
add(10, getOffsetNumber());

Functors

Functor is a concept from functional programming used to represent a container or wrapper around a value. A functor is an object that has a special method called map that allows you to apply a function to the value inside the container, and then return a new container with the transformed value.

const Identity = value => ({
  map: fn => Identity(fn(value)),
  valueOf: () => value
})

Functors can be useful in functional programming to create more expressive and abstract code, allowing us to separate the logic of the computation from the container holding the data.

const double = (x) => {
  return x * 2
}

const plusTen = (x) => {
  return x + 10
}

const num = 10

const doubledPlus10 = Identity(num)
  .map(double)
  .map(plusTen)

console.log(doubledPlus10.valueOf()) // Prints 30

This technique is very powerful because you can decompose your programs into smaller reusable pieces and test each one separately without a problem. JavaScript's Array object is also a Functor.

Monads

Monads are functors with an additional function flatMap operation. This helps in composing type-lifting functions.

Type lifting functions are the ones that wrap a value into some context.

Example of type lifting function:

const a=10;
const b=Array.of(a);
console.log(b) // prints [10]

Suppose you want to compose type-lifting functions. How will you do it without monads?

const Identity = value => ({
  map: f => Identity.of(f(value)),
  valueOf: () => value,
})

// The `of` method is a common type lifting functions to create a Monad object.
Identity.of = value => Identity(value)

// Lifting type of x to Identity. 
// These functions return object of type Identity
const squareIdentity = x => Identity.of(x ** 2)
const divideByTwoIdentity = x => Identity.of(x / 2)

const result = Identity(3)
  .map(squareIdentity)
  .map(divideByTwoIdentity)
  .valueOf()

console.log(result);

We cannot directly use map here because, the functions return Identity objects. We need to extract the values from Identity objects and then apply the function.

Monads come to rescue here:

const Identity = value => ({
  // Directly apply function over the value as it already return Identity
  flatMap: f => f(value),
  valueOf: () => value
})

Identity.of = value => Identity(value)

const squareIdentity = x => Identity.of(x ** 2)
const divideByTwoIdentity = x => Identity.of(x / 2)

const result = Identity(3)
  .flatMap(squareIdentity)
  .flatMap(divideByTwoIdentity)
  .valueOf()

console.log(result); // Logs out 4.5

Now we can compose type-lifting functions.

If you want to take a deep dive into monads, a great article is here.