DEV Community

Amanda Hasenzahl
Amanda Hasenzahl

Posted on • Updated on

Functional Programming: The Basics

In computer science, functional programming is a programming paradigm -- a way of thinking about software construction based on a set of fundamental, defining principles.

The fundamental, defining principles that make up this paradigm are that the code:

  • follows a declarative pattern
  • is composed of pure functions
  • avoids shared state, mutable data, and side effects

Imperative vs Declarative Pattern

Imperative Pattern

When the computer is given specific steps in order to achieve a desired result -- telling the computer exactly HOW to do something.

This tends to be the pattern that developers follow most often. It is the way that, we as humans, are used to trying to solve a problem.

Declarative Pattern

When the computer is given instructions on what result is desired without telling it exactly how it is to be done -- telling the computer WHAT needs to be done.

This is the way that functional programmers approach solving a problem. They focus on what results they need, rather than how the results are achieved. It is a different approach that can be hard to adopt at first, but can do significant things for your code.

Using a for loop and the imperative pattern vs. using Array.prototype.map() and the declarative pattern<br>

Both of these examples are adding new items onto each book object inside the books array.

The for loop example (Imperative Pattern):

  1. It’s checking array index counter against the array length
  2. Adding a lastRead property to the books object with the current date as the value for the currently indexed book.
  3. Incrementing the index counter for every time through the loop

It’s giving the computer a step by step instruction for how to add these new items

The .map() example (Declarative Pattern):

  1. Takes a function as an argument
  2. That function receives each item as a parameter
  3. Adds a lastReadBy property to each book with a string value of 'me'.

It’s giving the computer the information to produce the desired result, but it is not telling it exactly how to do it. The .map() method behind the scenes is taking care of the actual operation.

Pure Functions

  • accept at least one parameter
  • return something as a result
  • return the same output if given the same input
  • produce no side effects
  • are referentially transparent -- you can replace the function call with its resulting value without changing the meaning of the program

They are also simple and reusable building blocks for your code, completely independent from outside state therefore immune to state related bugs, as well as being easy to move around, refactor, and reorganize within your code. Thus making your overall program more flexible and adaptable to future changes.

Pure function that returns the sum of two given parameters

This is an example of a pure function. It accepts at least one parameter and returns a value. When it's given the values of 3 and 5, it will always return the output value of 8. It produces no side effects because the function relies on nothing except its input values.

A pure function call being replaced by its output as an example of referential transparency

This example shows a pure function and more specifically how they can be referentially transparent.

The add(x, y) function is taking in two values and producing their added sum as an output, which in this case is 8. Then, we have the multiply(a, b) function that is also taking in two values, but this time is producing their multiplied total as an output.

Using both functions we could write this function call as the first call multiply(2, add(3, 5));. Which would first add 3 to 5, producing the sum of 8. That sum of 8 would be passed as a parameter to multiply() along with 2, to produce the value of 16 as the final output.

We could also change the add(3, 5) function call as a parameter to just the value of its output (8). This change still produces the output value of 16. This replacement didn’t affect the output of the function in anyway, which makes it referentially transparent.

Immutability and Side Effects

Immutability

When an object cannot be modified in any way after it has been created.

The goal is to keep state and data from being shared or altered and solely keep it within the scope of each function, when possible.

There are no variable or loops, at least not how we are used to seeing them. Stored values are called variables because of history, but they are constants. Once x takes on a value, it is that value for life. They are usually local variables, so their lives are usually short, but while it is alive it can never change. Loops, on the other hand, happen through recursion.

One thing to keep in mind is that in JavaScript, the variable const doesn't necessarily equal immutability. const creates a variable name binding which cannot be reassigned after creation, however, it doesn't create immutable objects. You can't change the object that the binding refers to, but you can still change the properties of the object. This ultimately means that bindings created with const are mutable.

Recursion is when a function calls or refers to itself. This is used in place of traditional loops. Old values aren't modified during the looping, instead recursion uses new values calculated from the old ones. This allows constants and data to be modified as little as possible.

Flip book of a meteor striking Earth and killing the dinosaurs

Recursion is like a flip book. Each instance would be like each individual page of the flip book. They are completely independent of each other, don't modify anything on any of the other pages, and putting each instance together gives you the final result.

Blocks moving across a conveyer belt on an assembly line

Traditional loops are more like an assembly line. Each part of the process molds or changes the object until you get the final result. Each part is reliant on the one that comes before and after it and the final result is reliant on each part of the process and the order in which they are completed in.

A function that finds factorials using recursion

There are three key features in a recursion function.

  1. Termination Case
    It stops the function from happening infinitely. It is the emergency brake and is used to break out of the logic if you have reached the end of the input or if there is a bad input and you don’t want the code to run at all (in this example a negative number because there aren’t factorials for negative numbers). The termination case for this example is x < 0.

  2. Base Case
    Similar to the termination case, it is also used to stop the recursion from continuing. Base case however, is the goal of the function. In this example, x === 0 is the base case because once x has gotten down to 0, the factorial has been found and the recursion doesn’t need to go any further.

NOTE: In some functions, you will only see a base case instead of a base case and a termination case. This would be because the base case and the termination case are the same or can be taken care of in one single call.

  1. Recursion The function repeatedly calling itself until it reaches its base case. In this example, that is return x * factorial(x - 1);.

Step by step breakdown of the factorials recursion function with 3 as an input

This example breaks down as follows:

  1. We are calling the function and passing it the value of 3 → factorial(3);
  2. The function is run and since 3 is greater than 0, the function returns 3 * factorial(3-1) OR 3 * factorial(2)
  3. The function is run again with the value of 2 → factorial(2);
  4. Again 2 is greater than 0, so the function returns 2 * factorial(2-1) OR 2 * factorial(1)
  5. The function is then run again with the value of 1 → factorial(1);
  6. Once again it is greater than 0, so the function returns 1 * factorial(1-1) OR 1 * factorial(0)
  7. When the function is run another time with the value of 0, the base case becomes true, so the function returns the value of 1 (if (x === 0) return 1)
  8. Now that the function has finally finished, everything unwinds.
  9. IMPORTANT -- Recursion is a group of nested function calls, so the innermost function will return first (Last One In, First One Out)
  10. Everything unwinds in the order shown at bottom of the image above

Side Effects

Any application state changes that are observable outside the called function other than its return value.

Elements in your code that can cause side effects are:

  • modifying any external variable or object property
  • logging to the console
  • writing to the screen, a file, or the network
  • triggering any external process
  • calling other functions that contain side effects

Unfortunately, you can’t have a program or code base that is completely 100% free from side effects, but you can work to keep them contained and isolated within your code. This makes it easier to extend, refactor, debug, test, and maintain your code. It is also why front end frameworks encourage users to manage state and component renderings in separate, loosely coupled modules.

Shared State is something that will create side effects within your code if it is altered.

One reason for this is because it is impossible to know the entire history of every shared variable, especially if there are asynchronous calls happening within your code.

An example of this would be if there was a user object for your program that needed to be saved. The saveUser() function makes a request to the API on the server and while that is happening, the user changes their profile picture with the updateAvatar() function. This triggers a second request with saveUser(). Since these are async calls, if the second call is received first, when the first call (now outdated) call gets returned, the new profile picture will get deleted and replaced with the old one.

This is an example of a race condition, which is a common bug with having shared state. During that entire process there are times when you don't know what's happening to the user object. Therefore, sometimes you receive a result you weren't expecting.

Another reason is because when the order of the functions changes or they get moved around it causes a cascade of failures within your code.

Two functions with the same values and operations result in two different outputs when executed in a different order

The first half of this example is taking the value in x and first executing the x1() function which adds 1 to make x.val = 3. Then it is executing x2() which is multiplying that by 2 to make x.val = 6.

The second half is the exact same values and functions as the first, however the two functions get called in reverse. It starts with the value of 2, then it multiplies that by 2 to get 4, and then it adds 1 to that. This gives you a final result of 5.

Changing the order of the function calls on the exact same value, produced two different resulting values.

Summary

  1. Functional programming is a way to approach solving software challenges based on a set of fundamental, defining principles: follows a declarative pattern, utilizes pure functions, and avoids using shared state, mutable data, as well as creating side effects.
  2. The declarative pattern entails giving the computer what you are wanting as a result without telling it exactly how it needs to be done.
  3. Pure functions are simple reusable blocks of code that are completely independent from any outside state. They are immune to bugs related to state changes and help make your code flexible to future changes because they are easy to move around and refactor.
  4. Shared state, mutable data, and side effects are avoided as much as possible. Although, a program can never be completely free of side effects, the goal is to keep them contained and isolated inside your code.
  5. Adopting a functional programming approach in the right situations has potential to take your code to the next level

Latest comments (8)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.