How you're a functional programmer and you may not even realize it πŸ•΅οΈβ€β™‚οΈ

avoerman

Alex Voerman

Posted on June 22, 2021

How you're a functional programmer and you may not even realize it πŸ•΅οΈβ€β™‚οΈ

So you have that one hipster "functional programmer" coworker... They ramble on about their side projects in Elm, why JavaScript is too mainstream, how they've learned Haskell because it's a different way of thinking about things, and have tried explaining what currying and a monad is to you several times. With a slew of new terms and languages you haven't even heard of, it's easy to dismiss your coworker's functional programming ideas as fringe concepts.

You're a JavaScript developer that has heard about functional programming in passing, but haven't committed the time to dive into it fully. What if I told you are probably already a functional programmer in disguise? You're probably already using loads of functional programming concepts without even knowing it. Before we explore some ways in you're already writing functional code, let's define some basic functional programming concepts.

Functional programming dumbed down without much extra terminology

Ignore currying, monads, and other terms that are often associated with functional programming. Functional programming at its core is to code with pure functions. There's two rules of pure functions:

  1. The same inputs always return the same output. No matter how many times the function is called, what order it's called in, or what environment the function is running, it will always have a consistent output.
  2. The function does not have an effect on anything outside the function. No modifying the parameters, changing a variable outside of the function scope, or making http calls. This is often called no side effects.

Advantages of using pure functions over impure functions

  • Pure functions are more testable and predictable, because the same inputs return the same outputs.
  • Pure functions are usually more readable (easier to reason about), because you don't need to think about the effects of outside state on your function. Refactoring becomes easier; if you have a function that is confusing, you can write a unit test for the existing function and replace or rewrite it with the new implementation without worrying too much about breaking existing code.
  • Pure functions are usually more reusable. Once you start writing pure functions, they'll typically be smaller because you can't rely on outside state. Small functions typically only do one thing, so they're inherently more reusable across your application.

How you're already using pure functions

Chances are, you've written many pure functions before. Here are some clues that you're actually already a functional programmer.

Using [].map instead of [].forEach or a for loop

Like forEach or a for loop, map() iterates over an array. The difference is, map will not change (or mutate) the original array. Map always returns a new array. Let's take an example function that capitalizes all items in a list and implement it with for, forEach, and map:

const list = ['apple', 'banana', 'carrot'];
const capitalized = capitalizeList(list);
Enter fullscreen mode Exit fullscreen mode

Using for

function capitalizeList(list) {
  for (let i = 0; i < list.length; i++) {
    list[i] = list[i].toUpperCase(); 
  }

  return list;
}

Enter fullscreen mode Exit fullscreen mode

Using forEach

function capitalizeList(list) {
  let newList = [];
  list.forEach(item => {
    newList.push(item.toUpperCase());
  });
  return newList;
}
Enter fullscreen mode Exit fullscreen mode

Using map

function capitalizeList(list) {
  return list.map(item => item.toUpperCase());
}
Enter fullscreen mode Exit fullscreen mode

You may have written the third option before or prefer it due to its conciseness. It's also the most pure. The for loop example modifies the original array, so it is impure. The capitalizeList forEach example will always return the same input and output, but the forEach function inside of capitalizeList is not pure because it relies on outside state. The map example is completely pure; both capitalizeList and the map function do not produce any side effects. Preferring map over the other options means you're probably writing many pure functions.

Using filter, reduce, find, or a host of other array pure functions

Like map(), filter() and reduce() will also not change the original array.

Filter using for

function filterByLetter(list, letter) {
  for (let i = 0; i < list.length; i++) {
    if (!list[i].startsWith(letter)) {
      list.splice(i, 1);
    }
  }
  return list;
}
Enter fullscreen mode Exit fullscreen mode

[].filter

function filterByLetter(list, letter) {
  return list.filter(item => item.startsWith(letter));
}
Enter fullscreen mode Exit fullscreen mode

Finally, reduce can be used to take an array and turn it into a new data type.

Summing numbers using for

function sumNumbers(numbers) {
  let sum = 0;
  for (let i = 0; i < numbers; i++) {
    sum += numbers[i];
  }
  return sum;
}
Enter fullscreen mode Exit fullscreen mode

Summing numbers using [].reduce

function sumNumbers(numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}
Enter fullscreen mode Exit fullscreen mode

Reduce is a little more advanced and merits its own article, but understanding it and using it should help with building more pure functions. Here are some more examples of pure functions in JavaScript that you may have used before:

Pure Function What's it for?
[].map() Returning a new array of new items
[].filter() Filtering arrays
[].reduce() Morphing arrays into new data structures
[].find() Finding first occurrence of item
[].some() Checking if array has at least one item matching criteria
[].includes() Checking if array has at least one item matching raw param value
[].every() Checking if array has ALL items matching criteria
[].slice(start, end) Trims array at positions
[].concat() Merging two arrays together
[].join() Converting array to a single string
[].flatMap() Converting a 2D-array into a single array

It's worth mentioning some common impure functions that modify the original array:

Impure Function What's it for?
[].push() Adding to an array
[].pop() Removing an item from array
[].sort() Sorting
[].shift() Removing first item in array
[].unshift() Adding items to the beginning of the array
[].splice() Removing/replacing items in array
[].reverse() Reversing the order

Using const instead of var or let

JavaScript essentially phased out var and replaced it with let and const in 2016. If you've ditched var, you're already on the right track. let (and var) allows you to reassign variables:

let vegetable = 'asparagus';
vegetable = 'broccoli'; // valid JavaScript

var carb = 'rice';
carb = 'bread'; // valid JavaScript
var carb = 'beans'; // also valid JavaScript
Enter fullscreen mode Exit fullscreen mode

const will not allow you to reassign variables

const legume = 'chickpea';
legume = 'soybean'; // syntax error
Enter fullscreen mode Exit fullscreen mode

In the above capitalizeList example, notice how let is present in the impure examples. If you program only with const, you're forced to write more pure functions.

Using Object spread {...} notation

It's worth mentioning that const isn't completely immutable - you can still modify objects:

const snacks = {
  healthyOption: '',
  unhealthyOption: 'Cookies'
}

const addHealthyOption = (snacks, healthyOption) => {
  snacks.healthyOption = healthyOption;
  return snacks;
}

const newSnackObject = addHealthyOption(snacks, 'Edamame');

console.log(newSnackObject) // 😊 { healthyOption: 'Edamame', unhealthyOption: 'Cookies' }
console.log(snacks.healthyOption) // 😦 'Edamame'
Enter fullscreen mode Exit fullscreen mode

In this example, addHealthyOption mutated the original object. This can be avoided using the object spread syntax:

const addHealthyOption = (snacks, healthyOption) => {
   return {...snacks, healthyOption}
}

const newSnackObject = addHealthyOption(snacks, 'Edamame');

console.log(newSnackObject) // 😊 { healthyOption: 'Edamame', unhealthyOption: 'Cookies' }
console.log(snacks.healthyOption) // 😊 ''
Enter fullscreen mode Exit fullscreen mode

Using array spread [...x]

Like the above example, array spreading is similar to object spreading. Let's refactor the above example to take in arrays and look at the two implementations.

const snacks = {
  healthyOptions: [],
  unhealthyOptions: ['Cookies']
}

const addHealthyOptions = (snacks, healthyOptions) => {
  snacks.healthyOptions.push(healthyOptions);
  return snacks;
}

const newSnackObject = addHealthyOptions(snacks, ['Edamame', 'Hummus and Veggies']);

console.log(newSnackObject) // 😊 { healthyOptions: ['Edamame', 'Hummus and Veggies'], unhealthyOptions: ['Cookies'] }
console.log(snacks.healthyOptions) // 😦 ['Edamame', 'Hummus and Veggies']
Enter fullscreen mode Exit fullscreen mode

Notice how snacks was mutated. Writing this in a pure way can be accomplished by using the array spread feature:

const snacks = {
  healthyOptions: [],
  unhealthyOptions: ['Cookies']
}

const addHealthyOptions = (snacks, healthyOptions) => {
  return {
     ...snacks,
     healthyOptions: [...snacks.healthyOptions, healthyOptions]
  }
}

const newSnackObject = addHealthyOptions(snacks, ['Edamame', 'Hummus and Veggies']);

console.log(newSnackObject) // 😊 { healthyOptions: ['Edamame', 'Hummus and Veggies'], unhealthyOptions: ['Cookies'] }
console.log(snacks.healthyOptions) // 😊 []
Enter fullscreen mode Exit fullscreen mode

Summary

When we write pure functions (same input, same outputs and no side effects) we're doing functional programming. These features can help us write pure functions:

  • Using .map() and other array methods like filter, find, and reduce that do not modify the original array
  • Using const instead of let or var
  • Using {...x} or [...x] to create new objects and arrays

If you've used any of these features, you've probably already written many pure functions. You can call yourself a functional programmer. Inevitably, it becomes harder and harder not to produce side effects or rely on outside state in your functions. This is where advanced functional programming concepts like closures, higher order functions, and currying come in. I did not focus on these advanced topics, because if you're new to functional programming, you're probably not already currying functions on a day-to-day basis. After you've mastered the basics, check out some of these resources to take you functional programming game to the next level:

πŸ’– πŸ’ͺ πŸ™… 🚩
avoerman
Alex Voerman

Posted on June 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related