DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Principles of Functional Programming
James Robb
James Robb

Posted on • Updated on

Principles of Functional Programming

Functional programming is a paradigm which has its roots in mathematics, primarily stemming from lambda calculus. Functional programming aims to be declarative and treats applications as the result of pure functions which are composed with one another.

The primary aim of this style of programming is to avoid the problems that come with shared state, mutable data and side effects which are common place in object oriented programming.

Functional programming tends to be more predictable and easier to test than object oriented programming but can also seem dense and difficult to learn for new comers but functional programming isn't as difficult as it would at first seem.

The principles of Functional Programming

The aim of this article is look at the basic principles of functional programming and to clarify what functional programming is and what it stands for which should produce some clarity on the subject for those of you who are newcomers and a good refresher for everyone else.

Pure Functions

A pure function is a function which:

  1. Given the same inputs, always returns the same output
  2. Has no side-effects
// pure
function getSquare(x) {
   return x * x;
}

// impure
function getSquare(items) {
  var len = items.length;
  for (var i = 0; i < len; i++) {
    items[i] = items[i] * items[i];
  }
  return items;
}
Enter fullscreen mode Exit fullscreen mode

Basically, any function that changes its inputs or the value of some external variable is an impure function.

No side effects

Side effects are treated as evil by the functional programming paradigm. Side effects are things such as I/O, logging to the console, thrown and halted errors, network calls and the alteration of an external data structure or variable. Basically anything which makes a system unpredictable.

In saying this, functional programming doesn't say you can't have side effects since they are required at times but it aims to reduce the occurrence of such effects as much as is possible.

If a function contains side effects, it is called a procedure.

Immutability

Immutability is at the core of functional programming. Immutability is the idea that once a value is declared, it is unchangeable and thus makes behaviours within your programme far more predictable.

Referential transparency

Referential transparency is a fancy way of saying that if you were to replace a function call with its return value, the behaviour of the programme would be as predictable as before. Referentially transparent functions only rely on their inputs and thus are closely aligned with pure functions and the concept of immutability.

For example:

function two() {
  return 2;
}

const four = two() + two(); // 4
// or
const four = two() + 2; // 4
// or
const four = 2 + two(); // 4
// or
const four = 2 + 2; // 4
Enter fullscreen mode Exit fullscreen mode

All of these ways of generating the variable four show that the function two is referentially transparent since I can replace calling it with its return value and the programme would execute as expected.

Functions as first-class entities

This just means that functions are able to be passed as arguments to other functions, returned as values from other functions, stored in data structures and assigned to variables.

For example I could do the following since JavaScript treats functions as first class entities:

function add(left, right) {
 return left + right;
}

const adder = add;

adder(2,3);
Enter fullscreen mode Exit fullscreen mode

Higher order functions

Higher order functions are functions which do at least one of the following:

  1. Takes one or more functions as arguments
  2. Returns a function as its result

We have already some higher order functions in my previous articles such as those on Array Map, Array Filter, Array Reduce and Array Sort.

All other functions are called first order functions.

Disciplined state

Disciplined state is the opposite of shared, mutable state. An example of the drawbacks of shared, mutable state would be the following:

function logElements(arr) {
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}

function main() {
  const arr = ['banana', 'orange', 'apple'];

  console.log('Before sorting:');
  logElements(arr);

  arr.sort();

  console.log('After sorting:');
  logElements(arr);
}

main();
// Before sorting:
// "banana"
// "orange"
// "apple"
// After sorting:
// undefined
Enter fullscreen mode Exit fullscreen mode

Credit to 2ality for this example.

We can see that the second call produces no result since the first call emptied the input array and thus mutated the application state generating an unexpected output.

To fix this we turn to immutability and the use of copies to keep the initial state transparent and immutable.

function logElements(arr) {
  while (arr.length > 0) {
    console.log(arr.shift());
  }
}

function main() {
  const arr = ['banana', 'orange', 'apple'];

  console.log('Before sorting:');
  logElements([...arr]);

  const sorted = [...arr].sort();

  console.log('After sorting:');
  logElements([...sorted]);
}

main();
// Before sorting:
// "banana"
// "orange"
// "apple"
// After sorting:
// "apple"
// "banana"
// "orange"
Enter fullscreen mode Exit fullscreen mode

Encapsulation of state within individual functions, not changing external state or data structures and making use of shallow or deep copies and inputs will help you keep your state disciplined and predictable.

Type systems

By using types we leverage a compiler to help us avoid common mistakes and errors that can occur during the development process.

With JavaScript we could do the following:

function add(left, right) {
  return left + right;
}

add(2, 3) // 5
add(2, "3"); // "5"
Enter fullscreen mode Exit fullscreen mode

This is bad because now we are getting an unexpected output which could have been caught by a compiler. Let's look at the same code written with flow type annotations:

function add(left: number, right: number): number {
  return left + right;
}

add(2, 3) // 5
add(2, "3"); // error!
Enter fullscreen mode Exit fullscreen mode

Here we can see the compiler in action protecting us from such basic issues, of course much more is possible with a statically typed approach to developing but this should give you the gist of why it is useful to use.

Conclusions

Functional programming gives us some principles which make our code more readable, predictable and testable. This allows us to have code which contains less bugs, easier onboarding and a generally nicer codebase from my experience. In the following articles we will be looking at some functions which will help us develop more function driven applications.

Top comments (7)

Collapse
 
ilya_sher_prog profile image
Ilya Sher • Edited on

To fix this we turn to immutability and the use of copies to keep the initial state transparent and immutable.

Note / opinion: while use of copies serves the example (I guess), in real world one should definitely fix logElements not to modify the input parameter. Please don't leave bombs like this to your colleagues - logElements name does not suggest it would modify the elements array.

I would call the suggested "fix" a "workaround", which should only be made if you can't modify the logElements function.

Collapse
 
jamesrweb profile image
James Robb Author • Edited on

I agree that this isn’t good code or practice to use in a production environment and if seen in the wild it should be refactored but that was the point of the example.

Furthermore the example isn’t my own and instead comes from the linked source who uses this example to showcase a similar point.

The other point of consideration is that in some environments it is unavoidable to have mutations to the inputs and so clones and copies become the default solution to avoid mutation to the original values which this example showcases.

Collapse
 
ilya_sher_prog profile image
Ilya Sher

Furthermore the example isn’t my own

Really doesn't matter. I was commenting about the example, not about you :)

The aim is to let potentially inexperienced readers know that this is typically not OK.

The other point of consideration is that in some environments it is unavoidable to have mutations to the inputs

Unfortunately, yes.

and so clones and copies become the default solution to avoid mutation to the original values which this example showcases.

I would also like the readers to understand that it's more of a workaround than a solution.

Thread Thread
 
jamesrweb profile image
James Robb Author • Edited on

β€œI would also like the readers to understand that it's more of a workaround than a solution.” - only if it’s avoidable since in the environments where we can’t have proper immutability this is the solution.

Fair enough, it’s a point though. Thanks for the comments!

Thread Thread
 
ilya_sher_prog profile image
Ilya Sher

Mmm.. interesting philosophical topic. I really need to think about it. Thinking here now. I would say that it's a workaround, which is the appropriate solution for the situation (as opposed to refactoring the original function, which is not possible).

Keeping it in mind and in comments as "workaround" should potentially push more towards the "real" solution when it becomes possible.

I guess "workaround" is specific type of solution in this situation. Between two words when one is more specific, I choose the specific one unless I specifically want to generalize.

Collapse
 
pablocamacho profile image
pablocamacho

Hi, James.

While talking about "pure functions", you mentioned that the function getSquare( items ) is an impure one. Just because "(...) any function that changes its inputs or the value of some external variable is an impure function.". So, since we are receiving a container, changing its content and returning it already modified, leads to be working with an impure function.

What about if I perform an internal copy of the "items" parameter, like "items1", and then do exactly the same process your function does but over the "items1" container and finally I return "items1". Should now this be a pure function? If the answer is "no", which would be the reason? Another point here might be "because we have a loop inside it, which is prone to side effects", correct? But, if this is the case, we have another reason different from: "any function that changes its inputs or the value of some external variable is an impure function."

I have asked this same question (based on your post) to a well-known Developer, Teacher and Writer and he gave another definition to me of a "pure function": "A function should always give the same result when given the same arguments."

This definition is clearly different than yours.

Could you please clarify and explain this topic better?

Thanks in advance!

Collapse
 
jamesrweb profile image
James Robb Author

What about if I perform an internal copy of the "items" parameter, like "items1", and then do exactly the same process your function does but over the "items1" container and finally I return "items1". Should now this be a pure function?

As I stated in the article:

Any function that changes its inputs or the value of some external variable is an impure function.

So long as our inputs are unaffected and the output is consistent, the function is pure but if you alter variables you then break the rules of immutability but that's a different point.

If the answer is "no", which would be the reason?

If acting on an internal variable that is thrown away after execution, that is fine so long as the internal variable does not break the rules of pureness itself, for example mutating another internal variable such as:

function add(a, b) {
    let sum = a;
    sum += b;
    return sum;
}
Enter fullscreen mode Exit fullscreen mode

But it does break the rules of immutability which is why it should be avoided to do something like this. We never want to mutate variables but to create immutable deep copies or new values:

function add(a, b) {
    const start = a;
    const withB = start + b;
    return sum;
}
Enter fullscreen mode Exit fullscreen mode

This is a silly example since returning a + b would suffice but if you do need variables then immutable constants are the way to go.

Another point here might be "because we have a loop inside it, which is prone to side effects", correct? But, if this is the case, we have another reason different from: "any function that changes its inputs or the value of some external variable is an impure function."

Loops aren't inherintly bad constructs, they have uses but generally speaking whatever a loop can do, so can mappers, reducers, recursion and so on. For example in the article on the pure functions section I gave the example of calculating the squares of numbers, a pure way to do that without loops would be like so:

function getSquare(x: number): number {
   return x * x;
}

function getSquares(items: number[]): number[] {
  return items.map(getSquare);
}
Enter fullscreen mode Exit fullscreen mode

No loops required.

This definition is clearly different than yours.

Could you please clarify and explain this topic better?

As I stated in the pure functions section:

A pure function is a function which:

  1. Given the same inputs, always returns the same output
  2. Has no side-effects

The first point is the same as the definition the "well-known Developer, Teacher and Writer" stated.

If you need something else clarified though, feel free to ask!

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.