Functional Programming in a Nutshell (in JS)

Reading Time: 9 minutes

Functional Programming (FP) is a great tool to add to your toolbox! It enables you to write simple, predictable, immutable code. It even works nice with Object-Oriented Programming (OPP) —despite what some people will tell you. At the same time, it’s also easy to learn once you understand the core concepts. So, stay and read to take a peek about it.

 

Huh? Functional Programming?

Let's start with the basics:

To speak about Functional Programming we need to begin with the basics:

FP is a programming paradigm. It is to say, a way to rationalize a problem and model a solution, very much like Object-Oriented Programming. Click To Tweet

FP is a programming paradigm. It is to say, a way to rationalize a problem and model a solution, very much like Object-Oriented Programming.

Even though OOP and FP are not opposites (in fact, they work well together), the best way to wrap your head around FP is by comparing what they do differently. Both are equally capable of building a house; the difference is what kind of bricks they use.

Object-Oriented Programming

OOP uses Objects as its "Bricks". In other words, it uses blocks that wrap attributes and methods that often interact with each other and mimic the real world (kinda). We could say that OOP is concerned with building a logical structure, just like the bones of a body. Here's an example:

class Dog
  attr_accessor :name, :description, :favorite_toy
  
  def initialize(name, description, toy)
    @name = name
    @description = description
        @favorite_toy = toy
  end

  def change_favorite_toy(toy)
    @favorite_toy = toy
  end
end

So, in the example we described a template of a Dog. Then, we use it to create all kinds of fluffy friends:

...
charlie = Dog.new("Charlie", "he's very fluffy and my best friend", "plushies")
maya = Dog.new("Maya", "A small, energetic, black pug", "tennis balls")
cookie = Dog.new("Cookie", "Her coat looks like cookie ice cream", "shoes")
...

As we can see, each dog is an instance of an object that contains its own methods and parameters. So, for example, if we want to change the favorite toy of a dog, we invoke the method of each instance.

maya.favorite_toy >> "tennis balls" 
maya.change_favorite_toy("frisbee")
maya.favorite_toy >> "frisbee"

After that, we need to change (or mutate) the object into something different.
So, we think of problems as relationships and interactions between these objects. Thus, Object-Oriented Programming.

Functional programming

FP, on the other hand, uses Pure Functions as its building bricks. Pure Functions are just functions that are predictable, non-destructive, and have no side effects (we'll tackle each one of these in a bit). Furthermore, we use them to describe a process, and we don't care about building a data structure; instead, we care more about making the neurons of the program that should work regardless of the data we input.

Functional Programming strongly dissociates data from logic. Click To Tweet

We often say that FP strongly dissociates data from logic. So, if OOP tries to build the bones of the program first, FP prioritizes the nervous system.

Pure functions are deterministic

Each function’s output should depend only on its input, and things outside of the scope of the function are a big no-no. We want each function to be predictable: if you input a given value, you will always receive the same value back.

Pure functions have no side-effects

This basically means that they do not produce observable effects outside of the function. That means no mutation of local static variables, non-local variables, mutable reference arguments, or I/O streams (even console.log is considered as a side effect!). In short, the only thing that gets transformed is the input data.

Pure functions are non-destructive

They do not alter the data that was passed to them. We only want to describe how the input will be changed.

Let’s review a couple of examples of impure functions:

// 💩 our output doesn't depend entirely on our inputs! when we run it again
//we will get a different result
function getRandomNumber(num){
  return num * Math.random()
}

// 💩 we are introducing a side effect!
let budget = 100
function addToBudget(number){
    budget += number 
}

// 💩 this variable is outside the scope of the function!
var totalPeople = 10
function totalVotes(votes){
 return votes * totalPeople
}

// 💩 this function is destructive! 
function addCardsToDeck(deck, card){
    deck.push(card)
    return deck
}

All of those functions are impure! But we can do better. So, let's check some examples of pure functions.

// 👍 the result of the function depends entirely on its input 
function addToBudget(budget, number){
    return budget + number
}

// 👍 this function is non-destructive! 
//instead of altering the original array, we create a copy
function addCardsToDeck(deck, card){
    return deck.slice().push(card)
}

Much better!

But why go through the mental hurdle of rethinking functions, and restraining ourselves from using global variables? In the end, if the project works, it works, right?

Well, sure. But using pure functions gives us a plethora of advantages. For starters, they are so much easier to read since they require no context. Also, they receive all of the needed parameters upfront. And finally, you know each function is self-contained, and thus, following complex code is no problem.

We also get:

Testability: Since pure functions are deterministic by nature, writing tests for them is a lot simpler!

Parallel Code: Pure functions only depend on their input, and will not cause side effects, so they work great when we need threads running in parallel with shared memory.

Modularity and Reusability: Because our functions depend only on the input we feed them, we can think of them as little units of logic that we can easily reuse between different parts of our codebase, even in different projects.

Referential Transparency:Admittedly, this one is a bit complicated, so don't worry about understanding it completely just yet— Simply put, referential transparency means that a function call can be replaced by its output value, without changing the overall behavior of our program. This makes, amongst other things, unit testing much easier.

This sounds great and all, but can a program even be pure? With these restrictions, it sounds to me like you can never achieve purity — some of you, probably.

You're right, although pure functions offer tons of benefits, it's not realistic to make an application entirely out of them. If we did, our app would have no side effects, and wouldn't produce any observable results. Like a brain with no eyes, no mouth or no skin: perhaps pure, but unable to communicate and experience the outside world.

Instead, we need to make the bulk of the logic as pure as possible and delegate all the side effects to specific parts of the code.

Function composition: The super-power of FP

Disclaimer: There are many advanced techniques of Function Composition, this is just a first look.

There's yet another powerful tool that we need to review quickly. In FP languages, Functions are First-Class Citizens —yes, this is a programming concept. In a nutshell, it means that they support all the operations available to other entities, such as being passed as an argument, returned from a function, modified, and assigned to a variable. Living in first-class means you can be wherever you want. 🥂

null

Damn fine coffee by Andrew Neel

With their privileged status, we can easily compose a function with other functions as arguments, or return them to other functions! We can create very simple functions that serve a very specific purpose and chain them together to create more complex procedures. For example, we can start by teaching our program how to add 2 numbers together, to chain that method with itself to teach it how to multiply, to divide and calculate square roots, and to eventually have a complex brain capable of doing calculus!

With this in mind, let's try to make atomic functions and build complexity on our way up. After all, functions are our basic building blocks.

//our basic building block should be atomic functions
function sum(a, b){
    return a + b
}
function substraction(a, b){
    return a - b
}

function doubleOperator(f, a, b){
 return f(a, b) * 2
}

doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4

Ok — your brain.

Yes, admittedly, these aren't the flashiest examples possible. They may even seem pointless. Still, we start to see how to structure our programs based on functions, create basic blocks of processes, then link them together in more significant ways by using their status as first-class citizens.

Let's rewrite that code using ES6 arrow syntax:

const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;

const doubleOperator = (f, a, b) => f(a, b) * 2;

doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4

We are creating two functions that know how to sum and subtract, regardless of the input data. We are also composing them into a more complex one that knows how to double the result of a given operator, independently of what operator that is. Break down processes to Lego bricks and then build up from there. That's the basic principle of functional programming.

All software development is composition: The act of breaking a complex problem down to smaller parts, and then composing those smaller solutions together to form your application. — Eric Elliot


Lets bring it all together with a code example

Let's say that we are in charge of doggy day-care, and each day we have N dogs to pet. When we pet them, their happiness increases.

The OOP approach

Naturally, our priority is to describe our objects. We also need to create a method to pet them.

class Dog
  attr_accessor :name, :happiness
  
  def initialize(name, happiness)
    @name = name
    @happiness = happiness
  end

    def getPetted()
    @happiness += 100
  end
        
end

Now that we have our template, let's just create a bunch of data.

...
charlie = Dog.new("Charlie", 10 )
maya = Dog.new("Maya", 0 )
cookie = Dog.new("Cookie", 100 )
...
dogs = [... , charlie, maya, cookie, ...] 

Now we go through our daily routine.

def dailyRoutine
    dogs.each do |dog|
        dog.pet()
    end
end

There are some things to notice about this OOP implementation:

We supply data to each object at the time it is created (when we call the ‘new’ method). Then we use the methods we described in our template to interact with data that we’ve already stored. Finally, we create our daily routine method, which acts as a production line, transforming data, and taking decisions.

Let's see how FP changes things up:

The FP approach

We don't really care about emulating the real world: our data will just be an array, with no further structure required.

let dogs = [
    // ...  
    {name:"cookie", happiness: 10},
    {name:"maya", happiness:0 }, 
    // ...
    ]

We start by creating our tiniest bit of functionality. That is, increasing happiness.

const increaseHapiness = (happiness) => happiness + 100

This function will be used by other functions along the way.

Now we want to increase happiness whenever a dog is petted.

const petTheDog = (dog) => 
    Object.assign({}, dog, {happiness: increaseHapiness(dog.happiness)})

Note that we are creating a new object instead of mutating the original one. After that, we want to iterate over all the dogs and pet them all.

const petAllDogs = (dogs) => dogs.map(dog => petTheDog(dog))

console.log(petAllDogs(dogs)) 
// >> [ { name: 'cookie', happiness: 110 }, { name: 'maya', happiness: 100 } ]

Again, we are creating a copy of the array via Map instead of mutating the old one. We have effectively created a brain for our program by using functions as our basic building blocks. However, eagle-eyed readers might notice that I cheated a bit; in fact, this code is technically not pure; since it is calling an outside function.

This is considered a side effect! However, it is essential to remember that the objective here is not to turn you into a master functional machine; instead, I wanted to show you the ropes so you can start climbing on your own. If you are curious, this example would look a little bit more like this if we wanted to keep it Pure:

const increaseHapiness = (happiness) => happiness + 100

const petTheDog = (dog, increaseHapiness) => 
    Object.assign( {}, dog, {happiness: increaseHapiness(dog.happiness)})

const petAllDogs = (dogs, pettingMethod, increaseHapiness) => 
    dogs.map(dog => pettingMethod(dog,increaseHapiness))

console.log(petAllDogs(dogs,petTheDog,increaseHapiness))
// >> [ { name: 'cookie', happiness: 110 }, { name: 'maya', happiness: 100 } ]

This would be the most straightforward way to compose functions, but not the most scalable one. However, for now, let's be happy that our code is adhering better to functional programming principles.

So which approach is best?

Programming paradigms are another tool we can use. Click To Tweet

Programming paradigms are not a religion one needs to adhere to (not most developers anyway). Looking at it as either one or the other is the wrong way to think about it. It's better to see them just as another tool we can use.

That said, they do work exceptionally well in different types of problems. Michael Fogus, author of “Functional JavaScript,” in his blogpost “FP vs. OO, from the trenches,” proposes the idea that when he deals with data about people, FP works well, but when he tries to simulate people, OOP works well.

But even then, most of the time, your code quality will benefit from using the principles of both. After all, asking which is better is like asking if you would prefer a healthy body or a sharp mind. Why not both? Mix them, go nuts, follow your dreams!

Conclusion

Functional programming is a fantastic tool with many benefits! Learning it and applying at least some of its principles is simple enough that you can start today! And it will immediately bring the quality of your code a couple notches up. It’s also a considerable beast, that will need time and effort on your part to be completely tamed. This article only covers the basics; there's still tons of stuff about it you can learn.

0 Shares:
You May Also Like