Functional Programming Concepts

Functional Programming Concepts

Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.

~ John Carmack

In a recent technical interview that I had, I was asked about some programming concepts that leaned heavily on the functional programming paradigm. How did the interview go, you might ask. Well, if it went perfectly, I wouldn't be writing this post.

Whether you know a thing or two about functional programming, or you are team OOP, you might find the concepts that I will cover in this post to be interesting and useful. You can especially make use of these concepts in multi-paradigm languages (like JavaScript).

So what is functional programming?

Functional Programming is a programming language paradigm that makes use of functions and expressions without changing the state or data.

You might ask: well how am I going to get anything done without manipulating state or data?

To which my answer would be: you don't need to manipulate the state or data to get anything done.

It might be better to demonstrate this with code.

Here is a simple JavaScript code for saving text written by a user. Notice how we are mutating the text property in the doc object.

class WordDocument {
  constructor(text) {
    this.text = text;
  }
}

let doc = new WordDocument("");

function onSaveDocument(text) {
  doc.text = text;
}

Now, here is another piece of code that does not mutate the data:

class WordDocument {
  constructor(text) {
    this.text = text;
  }
}

let stack = new Stack(); // suppose we have a stack already implemented
let doc = new WordDocument("");
stack.push(doc);

function onSaveDocument(text) {
  let newDoc = new WordDocument(text)
  stack.push(newDoc);
}

In both code snippets we are updating the document's text. The difference is that in the first snippet we are directly changing the initial document object; in the second snippet, we are storing an entirely new document in a stack. The latter approach adds the benefit of rolling back the document state incase something goes wrong, or if the user simply wants to undo.

Here is an abstract variant of the second code snippet:

class State {
  constructor(property1, property2) {
    this.property1 = property1;
    this.property2 = property2;
  }
}

let stack = new Stack();
let initialState = new State("", "");
stack.push(initialState);

function onSave(property1, property2) {
  let newState = new State(property1, property2);
  stack.push(newState);
}

Such approach is what the functional paradigm enforces. It helps the programmer write robust and clean code. With that being said, let us look at other functional concepts in depth.

Pure Functions

Pure functions are functions that use local variables, and do not mutate external variables (they do not produce side effects in the program).

Here is an example of a pure function:

function add(n1, n2) { 
  return n1 + n2; 
}

The function is not modifying any external variables here. It is simply returning the result of an operation.

Here is an Impure function:

let goal = "";

function getGoalOfTheDay() {
  goal = "Read Don Quixote";
}

The above code produces a side effect to the outside world by modifying a non-local variable. In a large codebase, with many impure functions, such side effect can make your code less robust and maintainable.

We can fix the above function by writing it like this:

function getGoalOfTheDay() {
  return "Read Don Quixote";
}

let goal = getGoalOfTheDay();

Higher Order Functions

Higher Order Functions is a property of functional programming that allows us to pass a function as a parameter to another function.

A good example of HOF in regular use is the map function in JavaScript. It takes a callback function as a parameter:

const numbers = [1, 2, 3, 4, 5]; 
const result = numbers.map((number) => number *= 10);

Composition

Composition lets us use HOF's to take the output of one function as the input for another function.

A mathematical representation of that looks like this: f(g(x))

In code, function composition looks like this:

function divideByTwo(x) {
  return x / 2;
}

function addFive(x) {
  return x + 5;
}

function composedFunction(x) {
  return addFive(divideByTwo(x));
}

Here, the output of divideByTwo is used as the input for addFive.

Composition can be used in useful ways, like for processing images, where we can have a single function call a set of processor functions (e.g. a filter function, a resize function, etc.) like a pipeline.

Partial Application

Partial Application allows us to partially call a function with lesser arguments than is required. Sounds like magic, but there is an actual method to do that. Suppose a function requires N arguments, partial application lets us bind some arguments to produce another function that takes M arguments (where 1 <= M < N).

I think the example in this video explains partial application best:

function add(x, y, z) {
  return x + y + z;
}

function partial(fn, ...args) {
  return function (...moreArgs) {
    return fn(...args, ...moreArgs);
  };
}

const plus3 = partial(add, 1, 2);
plus3(5);

Currying

Currying is to process of breaking up a multi-argument function into a number of single-argument functions.

One major benefit of currying is memoization (not memorization). It essentially allows us to cache certain functions that are expensive to repetitively call.

Let us use the same example from the Partial Application section and curry it:

function addX(x) {
  return function addY(y) {
    return function addZ(z) {
      return x + y + z;
    }
  }
}

addX(5)(1)(2)

This is what a curried function looks like.

Now, let us look at a another example and suppose we have an expensive operation in a curried function.

function expensiveFunction(arg1) {
  const result = // expensive operations... 
  return function simpleFunction(arg2) { 
    return result == arg2;
  }
}

With currying, we can cache the result of the expensive function (that could be for example an api call), and we can reuse the result with multiple function calls without invoking the expensive function over and over again. Like so:

const isResultEqual = expensiveFunction("endpoint"); // expensive operation is called once and cached.

isResultEqual("simple function argument 1"); // uses cached result
isResultEqual("simple function argument 2"); // uses cached result

Conclusion

This sums up some of the fundamental concepts that the functional paradigm offers. Their application should be used thoughtfully to benefit from performance, maintainability, modularity, and security. That is it for this post. I hope you have found it helpful.

<- Back Home

Powered by Outstatic and Vercel

Berlin  4:14 PM