Profile Lucas Sta Maria

I'm a third-year Computer Science and Mathematics student at Northeastern University interested in programming languages and developer tooling.

(λx.x x) (λx.x x)

The Convenience of Currying

A curried function is a relatively simple idea: it's a series of unary functions returning unary functions, with the deepest unary function returning the computed final result. When currying is explained, usually a simple JavaScript example like below is used:

> // A function that adds two numbers in a curried fashion.
> const add = (num1) => (num2) => num1 + num2;
> add
[Function: add]
> add(1)(2)
3

> // We can now create a new function that adds 1 to a given number.
> const add1 = add(1)
> add1
[Function (anonymous)]
> // This returns the nested function from inside `add`, with `num1` being
> // set to 1. Now we can do something like:
> add1(5)
6

But, what's really the point of currying? Can't we have a regular binary function as follows?

> const add = (num1, num2) => num1 + num2;
> const add1 = (num) => add(1, num);
> add1(5)
6

Yeah, we totally could. What currying provides is convenience; we don't have to write an anonymous function every time to wrap the arguments. Observe the difference between the curried and non-curried versions of add1 again:

> // Non-curried `add`
> const add1 = (num) => add(1, num);
> // Curried `add`
> const add1 = add(1);

This is particularly useful when we use the curried functions as part of a larger computation. Let's switch gears to Racket.

;; add : Number -> Number -> Number
;; Add two numbers together, curried
(define add
  (lambda (num1)
    (lambda (num2)
      (+ num1 num2))))

;; add5 : Number -> Number
;; Add 5 to the given number
(define add5 (add 5))

See how simple it was to define the add5 function with a curried add function? The catch is that if we wanted to use add by itself, it's somewhat less readable:

> ((add 1) 2)
3

Racket, however, is a powerful language and comes with great built-in functions. One of them is specifically for transforming a function into a curried one: curry.

> ;; add : Number Number -> Number
> ;; Your standard binary addition function.
> (define (add num1 num2)
    (+ num1 num2))

> ;; (curry func args ...) takes in a function `func` and passes in the `args ...`,
> ;; which can be no arguments at all. It returns a curried function of `func` after
> ;; the `args ...` are passed in.
> (define add5 (curry add 5))
> (add5 5)
10

Addition isn't really a practical example for showing the power of currying, however. Currying is about delaying the computation of a function for later by only passing in a few of its arguments and letting the rest be passed in later. We call this partial-application – more formally where a function produces another function of a smaller arity (less parameters). Currying is a convenient way of achieving partial application.

Let's write a function contains?, specifically for lists of strings that determines if a string is contained within the list of strings.

;; contains? : (Listof String) String -> Boolean
;; Is the string contained in the list of strings?
(define (contains? los target-string)
  (ormap (lambda (s) (string=? target-string s)) los))

Hmm, wait a minute. We can actually introduce currying into this function – observe that in the anonymous function we pass into the ormap, we our target-string remains constant. Doesn't that mean we can partially evaluate our string=? and wait for each string s to fully evaluate?

;; contains? : (Listof String) String -> Boolean
;; Is the string contained in the list of strings?
(define (contains? los target-string)
  (ormap (curry string=? target-string) los))

Much cleaner: with currying, we don't have to wrap our string=? in an anonymous function. Let's consider another problem that uses the contains? function we defined. Let's say we have a list of people who can enter the club, aptly named cool-people. We want to filter a list of people trying to enter the club (queue) for only ones on the list of cool-people.

;; people-entering-club : (Listof String) (Listof String) -> (Listof String)
;; Filter for people in the `queue` who are in `cool-people`.
(define (people-entering-club cool-people queue)
  (filter (lambda (person) (contains? cool-people person))
          queue))

We can transform that lambda into a curried function, of course.

;; people-entering-club : (Listof String) (Listof String) -> (Listof String)
;; Filter for people in the `queue` who are in `cool-people`.
(define (people-entering-club cool-people queue)
  (filter (curry contains? cool-people) queue))

Here's the function in use:

> (people-entering-club '("John" "Jake" "Joe")
                        '("Jack" "John" "Joey" "Jerry" "Jake"))
'("John" "Jake")

In essence, currying is a powerful and convenient way of achieving partial application in a clean manner. It enables you to take "full" functions and delay their evaluation for later for when you get the rest of the arguments to pass in.