Currying, Part 1 - Function Composition

cdimitroulas

Christos Dimitroulas

Posted on August 2, 2021

Currying, Part 1 - Function Composition

Note: This post is also available on my personal blog. If you prefer to read it there, please click here

Introduction

Currying is a well known functional programming (FP) technique, often baked into FP languages like Haskell by default. When we say a function is curried, what we mean is that the function has been transformed from taking multiple arguments into a series of functions which each take a single argument. Imagine a function with 3 arguments f(a, b, c). The curried version of that function would instead be called like so: f(a)(b)(c).

The most common question that is asked after learning about currying is "What's the point?". The purpose of this series will be to look at some examples which illustrate the benefits of currying in real world applications. All code shown in the posts is written in Typescript and will also be available in this repository.

If you're looking for a more detailed explanation of what currying is and how to use it, please take a look at the chapter on currying in Mr. Frisbee's Mostly Adequate Guide to Functional Programming or this video on the FunFunFunction channel.

Function Composition

In functional programming, a lot of our time is spent composing smaller functions together into larger functions, also known as pipelines of functions. If we can find ways to compose our various functions together more easily, this could bring significant benefits to the way our programs are structured.

Let's imagine we are working on a web application which lets users review video games they have played.

Reviews have a couple of validation and sanitization rules to keep the data consistent and safe:

  • Reviews cannot be longer than 5000 characters
  • Reviews can only contain certain html tags, additional tags will be stripped.

Additionally, due to a pervasive toxic culture in many gaming circles, the company has decided they will add some rules on what is allowed in reviews:

  • Swear words will be replaced by a cat emoji.
  • No more than one exclamation point in a row. Reduce multiple exclamation points to a single character e.g. "!!!!" becomes "!"

Here are some functions and utils which will help us meet those requirements, written without currying:

const checkStringLength = (
  length: number,
  str: string
): string => {
  if (str.length < length) {
    return str
  }

  throw new Error(`String longer than max length (${length})`);
};

// implementation omitted for brevity
declare const stripHtml:  (
  allowedTags: string[],
  html: string
) => string 

const allowableHtmlTags = ["p", "h1", "h2", "a"];

// no, I'm not really gonna list the swear words here
const swearWords = ["poop"];

const catEmoji = "🐈";

const replaceWordWithCat = (str: string, word: string): string =>
  str.replace(new RegExp(word, 'g'), catEmoji)

// Matches all instances where there are 2 or more "!" characters in a row
const multipleExclamationMarks = /\!{2,}/g;
Enter fullscreen mode Exit fullscreen mode

We want to define a function handleReview which combines all of the above. Here is a simple implementation.

const handleReview = (review: string) => {
  const correctLenReview = checkStringLength(5000, review);

  const strippedHtmlReview = stripHtml(
    allowableHtmlTags,
    correctLenReview
  );

  const noSwearWordsReview = swearWords.reduce(
    replaceWordWithCat,
    strippedHtmlReview
  );

  const noMultiExclamationMarksReview = noSwearWordsReview.replace(
    multipleExclamationMarks,
    "!"
  );

  return noMultiExclamationMarksReview;
}

/**
 * Example usage
 */
handleReview("<p>hello</p><script>window.alert('You\'ve been hacked!')</script>");
// --> "<p>hello</p>"

handleReview("The devs who made this game are poop!!!");
// --> "The devs who made this game are 🐈!"

handleReview(new Array(5000 + 1).join("a"));
// --> Error: String longer than max length (5000)
Enter fullscreen mode Exit fullscreen mode

This works, but there are several issues with this implementation:

  • It is difficult to scan through it and gain a quick understanding of the business rules.
  • We needed to define intermediate variables (e.g. noSwearWordsReview and noMultiExclamationMarksReview) at each step which required coming up with clear variable names.
  • There is the potential to make a mistake and pass the wrong variable to a function. You could even accidentally return the wrong variable, such as correctLenReview instead of noMultiExclamationMarksReview!

We have used all of our smaller functions within handleReview, but wiring them up together is done in a very explicit and manual way. Function composition doesn't need to be so tedious! Let's try composing our functions using a utility like pipe/flow to simplify the process.

Note: flow is a function which allows us to compose multiple functions into a pipeline. Each function is ran in order, passing it's output onto the next function in the pipeline. The implementation I use the most is defined as flow in the fp-ts library. I don't recommend using pipe/flow from lodash as it is not type-safe at all and can easily introduce bugs into your program.

const handleReview = flow(
  (review: string) => checkStringLength(5000, review),
  correctLenReview => stripHtml(allowableHtmlTags, correctLenReview),
  correctLenAndStrippedReview => swearWords.reduce(
    replaceWordWithCat,
    correctLenAndStrippedReview
  ),
  nonSwearingReview =>
    nonSwearingReview.replace(multipleExclamationMarks, "!")
);
Enter fullscreen mode Exit fullscreen mode

The use of flow eliminates the potential for mistakes, so we have made some improvement to our implementation. It is no longer possible to accidentally return review directly, or to pass the wrong argument to one of our functions. However, we still have the issues with legibility and intermediate variables that the original implementation had.

If only we had a way to compose our functions in a better way, we could eliminate the remaining problems with the implementation. It almost feels like we have a bunch of puzzle pieces that don't quite fit together properly. Luckily, we are programmers... so we can just create new puzzle pieces that fit our needs!

Let's see what happens if we rewrite our original functions to be curried:

const checkStringLength = (
  length: number
) => (str: string): string => {
  if (str.length < length) {
    return str;
  }

  throw new Error(`String longer than max length (${length})`);
};

declare const stripHtml: (
  allowedTags: string[]
) => (html: string) => string;

// Curried version of String.replace
const replace = (
  searchValue: string | RegExp
) => (replacement: string) => (str: string): string =>
  str.replace(searchValue, replacement);

// Curried version of Array.reduce
const reduce = <Accum, T>(
  fn: (accum: Accum, x: T) => Accum
) => (list: Array<T>) => (initial: Accum) =>
  list.reduce(fn, initial);
Enter fullscreen mode Exit fullscreen mode

With our new curried functions in hand, we can re-implement our handleReview function like so:

const handleReview = flow(
  checkStringLength(5000),
  stripHtml(allowableHtmlTags),
  reduce(replaceWordWithCat)(swearWords),
  replace(multipleExclamationMarks)("!"),
)

handleReview("<p>hello</p><script>window.alert('You\'ve been hacked!')</script>");
// --> "<p>hello</p>"

handleReview("The devs who made this game are poop!!!");
// --> "The devs who made this game are 🐈!"

handleReview(new Array(5000 + 1).join("a"));
// --> Error: String longer max length (5000)
Enter fullscreen mode Exit fullscreen mode

Our handleReview function is now much easier to scan and understand at a glance. We have eliminated intermediate variables (reducing the mental effort to come up with good variable names) and didn't need to use any arrow functions to wire up function arguments correctly. Overall, the result is much better.

Conclusion

In this post we've taken a look at how currying can be used to improve function composition. By making it easier to compose our smaller functions into bigger ones, we gained several benefits:

  • Reduced mental load by removing the need to come up with names for intermediate variables
  • Improved the readability of our code, making it easier for future developers to understand the business logic of our domain
  • Eliminated an entire category of potential bugs by avoiding the task of manually composing our functions together

In the next post, we will look at a technique called partial application which is closely related to currying and how it can be used for handling user permissions.

As always, I look forward to your questions, comments and feedback!

💖 💪 🙅 🚩
cdimitroulas
Christos Dimitroulas

Posted on August 2, 2021

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

Sign up to receive the latest update from our blog.

Related

Currying, Part 1 - Function Composition
functional Currying, Part 1 - Function Composition

August 2, 2021