Currying, Part 1 - Function Composition
Christos Dimitroulas
Posted on August 2, 2021
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;
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)
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
andnoMultiExclamationMarksReview
) 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 ofnoMultiExclamationMarksReview
!
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, "!")
);
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);
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)
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!
Posted on August 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.