Wielding Pure Functions in JavaScript and Function Composition

Peleke Sengstacke
👁️ 1,619 views
💬 comments

Today, I'd like to share some thoughts on two fundamental concepts in functional programming: Pure functions and function composition.

After reading, you'll be able to:

  • Write pure functions, and explain basic advantages of purity
  • Isolate impure and pure operations in your JavaScript

Understanding purity is an important prerequisite to cultivating a functional mindset—I'd go so far as to say it's the prerequisite to understanding functional programming, period.

Table of Contents

    So: Let's talk purity.

    Pure Functions

    Two things need to be true for a function to be pure.

    First condition: A function can be pure if and only if the only thing it uses to calculate its output are the arguments you pass it, and local variables declared inside of the function itself.

    As usual, this is clearer by example than explanation.

    function greetUser (user, greeting='Hello') {
      return `${greeting}, ${user.firstName} ${user.lastName}!`
    }
    
    const joe = {
      firstName: 'Joe',
      lastName: 'Schmoe'
    }
    
    greetUser(joe) // 'Hello, Joe Schmoe!'

    Note that the only thing greetUser uses to calculate its return value is the user object we pass as argument.

    Second condition: A function can be pure if and only if it only if it does not change state outside of its own scope.

    The greetUser function above satisfies this condition: It takes a user, then returns a string. It doesn't touch state at all.

    Let's see an example where this doesn't hold true.

    const joe = {
      firstName: 'Joe',
      lastName: 'Schmoe'
    }
    
    function impureUpdate () {
      joe.firstName = 'JOE'
    }
    
    joe.firstName // 'Joe'
    
    // A
    unsafeUpdate() // changes `joe`'s `firstName` property
    
    // B
    joe.firstName // 'JOE'

    Note that joe is declared in the global scope—not in the local scope of impureUpdate. Yet, calling impureUpdate (B) changes the value of joe.firstName.(C).

    Thus, we see that calling impureUpdate changes state outside of the function's execution environment. These changes persist after the function has returned.

    If a function does change state outside of its scope, as does unsafeUpdate, it is said to have side effects. So, another, more common way of stating this condition is: A function can only be pure if it has no side effects.

    By the way, side effects include things like I/O and printing to the console. So, anything involving console.log, fs.readFile, etc., is technically "impure". But don't worry about that; we'll loop back to this little kink soon.


    To sum up, then, a function f is pure if and only if it meets these two conditions:

    1. f uses only its arguments and local variables to compute its result; and
    2. f has no side effects.

    Let's talk about why we should care, and then take a look at how writing pure functions.

    Advantages of Purity: Or, Disadvantages of Impurity

    The logical next question is: So what?

    For us, the most important reasons this matters are:

    1. Pure functions are "predictable", and thus easy to test.
    2. Pure functions simplify state management.

    ...Predictably, there are a lot of other advantages to purity, but we'll focus on these. rDrop your favorite in the comments if I missed it.

    Pure Functions are Predictable

    Recall the two conditions from above.

    A function is pure if it:

    1. Uses only its arguments and local variables to compute its result; and

    2. Has no side effects.

    Let's rephrase that.

    A function is pure if it:

    1. Does not use non-local variables or outside state to compute its result; and
    2. Does not change external state as it runs.

    This helps clarify what we mean by "predictable". Pure functions are predictable, in the sense that they will always return the same output for a given input.

    Examples help. First, our pure function from above.

    function greetUser (user, greeting='Hello') {
      return `${greeting}, ${user.firstName} ${user.lastName}!`
    }
    
    const joe = {
      firstName: 'Joe',
      lastName: 'Schmoe'
    }
    
    // This is boring...Same. Thing. Every. Time.
    greetUser(joe) // 'Hello, Joe Schmoe!'
    greetUser(joe) // 'Hello, Joe Schmoe!'
    greetUser(joe) // 'Hello, Joe Schmoe!'

    If we pass joe as its argument, the function greetUser will always return 'Hello, Joe Schmoe!'...Until the end of time.

    As you'd probably guess, not all functions are "predictable" in this way.

    
    function greetUserWithTime (user, greeting='Hello') {
      const salutation =`${greeting}, ${user.firstName} ${user.lastName}!`
      const currentTime = `The current time is: ${Date()}.`
    
      return `${salutation} ${currentTime}`
    }
    
    const joe = {
      firstName: 'Joe',
      lastName: 'Schmoe'
    }
    
    // Now, the result is different on every call
    greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 16:59:11 GMT-0500 (Eastern Standard Time)'
    
    greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 17:02:11 GMT-0500 (Eastern Standard Time)'
    
    greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 17:04:16 GMT-0500 (Eastern Standard Time)'

    Notice that, this time around, our function returns a different result each time we call it. It does this in spite of the fact that we pass the same argument upon each invocation.

    The reason we get different return values is because, instead of relying only on local variables and arguments, greetUserWithTime relies on external state—namely, the current time at the moment the function is called.

    Since the time changes each time we call greetUserWithTime, its return value does, as well. This makes it "impure", and unpredictable: To know what it will return ,we need to know both the arguments we pass; and the time of invocation.

    This is one of the major disadvantages of impure functions. To understand what they do, we need to know which pieces of system state influence their output. To make things worse, we need to know which pieces of the system influence those pieces of the system...And so on.

    A pure function's output, however, is determined solely by its inputs. This makes it "predictable", because its outputs are stable: In other words, the return value it generates for a given input will never change. All we need to know to understand a pure function is how it manipulates the data we pass it—information about the rest of the program is irrelevant.

    The fact that pure functions are predictable means they're easy to test.

    Consider greetUser. It's easy to predict what this function should output for any given user object. A (very simple) test might look as follows:

    const joe = {
      firstName: 'Joe',
      lastName: 'Schmoe'
    }
    
    const expected = 'Hello, Joe Schmoe!'
    const actual = greetUser(joe)
    
    if (expected === actual) {
      console.log('All good.')
    } else {
      console.error('ERROR. Expected: ${expected}...But got: ${actual}.`
    }

    A full test suite would address edge cases, etc., but the concept is clear: Expected behavior is easy to verify.

    Contrast this with greetUserWithTime. It's immediately unclear how to handle the fact that its output will be different every time. One standard solution is to mock the system time—i.e., "rig" the test environment such that Date() returns a value you specify—but that's a lot of complexity for an otherwise trivial task.

    The fact that pure functions neither influence nor rely upon external state means they simplify state management—we don't have to think about calling a pure function will affect the state of other components of our program, because they don't affect them at all.

    Managing Impurity

    Here are things you can't do when writing pure functions:

    1. Modify global state. This one is fairly obvious from the above examples.
    2. I/O. Reading/writing files is a side effect, so it's not allowed in pure functions. Logging to the console is also a side effect. Yes, that means pure functions can't contain console.log statements.
    3. AJAX. A network request to the same API endpoint could result in either the data you asked for, or an error.

    That restriction on I/O and AJAX seems limiting. But, keep in mind—there's nothing wrong with "impure" functions (hence the quotes). It's just that using them promiscuously can make programs more difficult to understand.

    The general rule of thumb is to manipulate data using only pure functions; and then render the results of those manipulations in impure functions.

    In other words, rather than strive to write only pure functions—which is impossible—we instead impose the discipline of separating pure and impure operations.

    Developing a sense for where impurity occurs in your code is a crucial step towards developing a functional awareness.

    Isolating Impure Functions

    Let's reiterate a reasonable approach to designing with pure functions:

    1. Before coding, identify what data you need. The user model? Database entries for a customer's recent bank transactions?
    2. Identify the source of your data, and load it. This will generally involve "impurity" in the form of I/O, AJAX, or monitoring of user interaction.
    3. Transform the data into the form your app requires. This might involve extracting and combining only the pieces of data you need, such as the firstName and lastName properties in greetUser above; deriving values from it, such as a customer's average spending over the past month; etc. Ideally, this should involve pure functions.
    4. Identify the parts of your code that involve "impurity"—I/O, AJAX, rendering to the DOM, etc.—and write a function that encapsulates the side effects. This function will obviously not be pure. Pass the data it needs as an argument—emphatically, do not manipulate the data inside this function.

    Let's apply these steps to developing a simple UI that dumps a random GIF on the page based on a user-entered search value.

    I'd initially done this in React, but idiomatic usage of this.setState confused the point. So, here it is in vanilla JS, instead.

    Before we get started, let's initialize some of top-level variables we'll be dealing with.

    'use strict';
    
    const image = document.querySelector('#image')
    const refresh = document.querySelector('#refresh')
    const input = document.querySelector('#search-term')
    
    const state = {
      searchTerm: 'surprise'
    }

    Here, we're fetching:

    • The image tag, where we'll dump our GIF
    • The refresh button, which resets the image; and
    • The input element, where users will enter their query.

    We've also created a global state object, where we'll store our searchTerm.

    Identifying the Data

    We need two pieces of data to make this work:

    1. A user-entered query
    2. A URL for a GIF

    We'll use the user-entered query to make an AJAX call to the GIPHY API. We'll use the URL provided in the response to set the src tag of an img attribute on the page.

    Neither function will be pure, as they depend on external state.

    Retrieving the Data

    The next step would be to implement functions for retrieving the user-entered query, and sending the AJAX request.

    The simplest way to capture user input is, of course, to set a listener on the input which updates the state on keyup.

    The simplest way to send the initial AJAX request is with fetch (or your favorite AJAX utility). We'll also create a (pure) buildUrl function for putting together the GIPHY query.

    // @impure: handler has side-effects
    input.addeventlistener('keyup', function keyuphandler (event) {
      setsearchterm(event.target.value)
    })
    
    // @impure: dom manipulation
    refresh.addeventlistener('click', function clickhandler (event) {
      updateimage(state.searchterm, image)
    })
    
    // @impure: has side-effects (updates global state)
    const setsearchterm = searchterm => {
      state.searchterm = searchterm
    }
    
    // @pure
    const buildurl = searchterm => {
      const url='http://api.giphy.com/v1/gifs/search'
      const apikey = 'fthi8mvnpuogqtuwmugzbdherk0etrqq'
    
      return `${url}?q=${searchterm}&api_key=${apikey}`
    }
    
    // @impure: time-dependen--whether we receive an error response or data depends on network
    const fetchurl = url => fetch(url)

    Manipulating the Data

    Our fetchGif function will return a Promise, which will resolve with a response (or error, which we'll ignore for now).

    That response will contain a bunch of information provided by the API. We only need one piece: the URL for the GIF.

    In this case, the only manipulation we need to do is pluck the value of this one property, and throw away the rest.

    // @pure
    const pluckImageUrl = gif => gif.images.original.url

    This pluckImageUrl function returns the URl we'll need to use to update our image's src attribute.

    Updating DOM

    The last thing to do is update the value of our image tag's src attribute.

    We'll do this within the then clauses following fetchUrl. We've also created a (pure) randomIdex function, which allows us to pick a random image from the list of GIFs provided by the GIPHY API.

    // @pure
    const randomIndex = ({length}) => Math.floor(Math.random() * length)
    
    // @impure: Performs AJAX, updates DOM
    const updateImage = (searchTerm, image) => {
      fetchUrl(buildUrl(searchTerm))
        .then(response => response.json())
        .then(data => {
          const gifList = data.data
          const index = randomIndex(gifList)
    
          const gif = gifList[index]
          image.src = gif.images.original.url
        })
        .catch(error => console.error(error))
    }

    ...And that's it. [You can clone the repo and open index.html to run it if you'd like]().

    The point, here, is that separating pure and impure functions helps make things more readable, more modular, and more testable. These advantages are less apparent in this example than they are in larger codebases, but the potential improvements to readability and modularity should be apaprent.

    Note that extracting pure functions and using them in functions like updateImage makes it easier to isolate bugs. I often see students approach problems like this by setting a click handler on the reresh button, then fetching the user's search term; building the URL; sending the AJAX request; and updating the DOM, all in that handler.

    This usually ends up being cumbersome to debug, even in simple cases: If things don't work, it's not obvious which of the four pieces is broken. Writing four functions, each of which does one thing, makes it easier to identify exactly which piece is broken.

    Summary

    And there we have it: Purity, in a nutshell. Let's review the high points.

    • A function has side effects if it changes external state.
    • A function is pure if 1) its output is determined soley by its input values, and 2) it has no observable side effects.
    • Pure functions cannot perform I/O, AJAX, DOM manipulations, or logging.
    • Pure functions are (generally) easier to test than those that are dependent on external state.
    • "Impure" functions aren't intrinsically bad—hey're just potentially confusing.
    • Isolating pure and impure operations is good practice.

    Turns out, there are much deeper reasons that purity is compelling:

    • Parallelizing the execution of pure functions is (relatively) easy: Since they don't modify external state, they don't experience race conditions due to shared state.
    • Because a pure function's return value is determined completely by its inputs, they're easily memoized. If you're not familiar with memoization, it's worth reading about.
    • Because pure functions are, in a sense, well-behaved, compilers can make assumptions about their behavior that they can't make about functions with side effects. This allows them to make optimizations that would be impossible in the absence of purity, such as data structure fusion.

    These are all advanced concepts relatively uncommon in the world of JavaScript, but they do serve to illustrate just how powerful purity can be.

    Now that we've gotten this prerequisite out of the way, we can do some heavy lifting. Next time, we'll dive into function composition, and bring at least one of these advanced concepts—fusion—into our everyday JavaScript.

    Stay tuned.

    Peleke Sengstacke

    15 posts

    Peleke Sengstacke is a web and Android developer with a soft spot for functional programming.

    He likes linguistics, Haskell, and powerlifting. He dislikes mosquitoes, cramped bus rides , and merge conflicts.

    Catch him on Twitter (@PelekeS), or sign up for his email list on what to learn and where to learn it (http://www.tinyletter.com/PelekeS). It's wicked educational.