tech

Pure Functions - Practical Functional Programming | Part 2

Learn Pure functions in Functional Programming. Why using Pure functions, what a pure function is, and how to write code using pure functions.


Sandro Maglione

Sandro Maglione

Software development

Pure functions are one of the core principles of Functional Programming. In this article, you are going to learn why using pure functions, what a pure function is, and how to implement pure functions in your code.

Practical Functional Programming step by step is a series in which we are going to uncover the principles of functional programming and effective coding one small piece at the time.

Starting from a complete code example, we are going to learn step by step what makes a good functional code and what are the benefits of applying a functional programming paradigm to your codebase.


Review assignment: Find repeated characters

In the last post, we introduced the problem we have been assigned to solve, of course using functional programming. The problem statement is the following:

Given a String, find all the characters that are repeated 2 or more times and return a new String containing only these characters.

We then explored the solution to the assignment using functional programming in three different languages: Haskell, Typescript, and Dart.

Today we are going to introduce the first core principle which sits at the core of functional programming: Pure Functions.

Why would we need Pure Functions

The first question starts always with why: why should I write Pure Functions?

Pure Functions are based on the mathematical definition of a function. In math, every function takes some inputs and based only on these inputs it produces an output. A mathematical function cannot access global variables or mutate any state outside itself.

This simple assumptions unlock some interesting considerations:

  • Every function is self-contained; it does not need to know anything about the outside world and it cannot modify it (immutability!)
  • Every function can be treated as a black box; you can look at a function in isolation and understand it without knowing anything about the current global state
  • Same inputs, same output; Since a Pure function has access only to its inputs and anything else, the output given the same inputs is always the same

We can see why Pure functions are beneficial in coding. Pure functions allow local reasoning. You can focus on one single function in isolation without the need to understand the global state of the application. Furthermore, since every function can be treated as a black box, often times the signature of a function is all you need to know to effectively use it.

Pure functions in our assignment's solutions

All three of the solutions I proposed you in Part 1 are Pure functions. Let's explore the buildmap function in dart as an example:

Map<String, int> buildmap(String str) => str.split('').foldLeft(
      <String, int>{},
      (acc, x) => {
        ...acc,
        x: (acc[x] ?? 0) + 1,
      },
    );

We are going to review the principles of Pure functions using this simple example:

  • Self-contained: buildmap does not access any variable outside of the str input. Moreover, the functions split and foldLeft do not modify the original variable (immutability)
  • Black box: Since the function is based only on its input, you can effectively forget about the implementation and use it without worrying about any side effect. Moreover, you do not need to know anything about the context of the application in which this function is used.
  • Same input, same output: You can call the function an infinite number of times with the same input, you will get always the same output

The same considerations are valid also for the Typescript and Haskell solutions:

// Use the functions split, reduce, modifyAt, and upsertAt
// Output based only on the str input
const buildmap = (str: string): Map<string, number> =>
  pipe(
    str.split(''),
    reduce(new Map<string, number>(), (acc, x) =>
      pipe(
        acc,
        modifyAt(eqString)(x, (n) => n + 1),
        O.getOrElse(() => pipe(acc, upsertAt(eqString)(x, 1)))
      )
    )
  );
-- Use functions foldl, member, adjust, and insert
buildmap :: String -> Map Char Int
    buildmap =
      foldl
        ( \acc x ->
            if member x acc
              then adjust (+ 1) x acc
              else insert x 1 acc
        )
        empty

Remember, these three functions are exactly the same, even if they are written in different languages:

  • foldLeft (dart), reduce (typescript), foldl (haskell)
  • split (dart, typescript)
  • modifyAt (typescript), adjust (haskell)
  • upsertAt (typescript), insert (haskell)

Hopefully you now have a more clear idea of what a Pure function is and also why it is useful to write Pure functions. We are going to move one principle at the time. That's it for this Part 2.

See you soon for Part 3!

👋・Interested in learning more, every week?

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.