1. Overview

In this tutorial, we’ll look at how Scala supports currying. We’ll see how it differs from the partial application function and the advantages of using each approach.

2. Currying

Currying is the process of converting a function with multiple arguments into a sequence of functions that take one argument. Each function returns another function that consumes the following argument.

2.1. Function

First, let’s create a function with two arguments, and convert it to a curried function.

val sum: (Int, Int) => Int = (x, y) => x + y 
val curriedSum: Int => Int => Int = x => y => x + y
curriedSum(1)(2) shouldBe 3

There is even a special curried method with multiple arguments that we can use.

val sum: (Int, Int) => Int = (x, y) => x + y
val curriedSum: Int => Int => Int = sum.curried
curriedSum(1)(2) shouldBe 3

2.2. Method

We can achieve the same for methods because Scala gives us the ability to create multiple argument lists.

def sum(x: Int, y: Int): Int = x + y
def curriedSum(x: Int)(y: Int): Int = x + y

There’s even a way to convert a method with multiple arguments to curried function.

def sum(x: Int, y: Int): Int = x + y
val curriedSum: Int => Int => Int = (sum _).curried

sum _ converts a method with multiple arguments into a function with multiple arguments (called eta-expansion) on which we can call curried.

Also, note that eta-expansion will convert multiple argument lists method into a curried function without the curried call:

def sum(x: Int)(y: Int): Int = x + y
val curriedSum: Int => Int => Int = sum

3. Partial Application

Partial application is the process of reducing the number of arguments by applying some of them when the method or function is created. Let’s use partial application and curriedSum function to create the increment function.

val curriedSum: Int => Int => Int = x => y => x + y
val increment: Int => Int = curriedSum(1)

The same can be done using methods.

def curriedSum(x: Int)(y: Int): Int = x + y
val increment: Int => Int = curriedSum(1)

4. Type Inference

Type inference takes into account only one parameter list at the time. That means that in some cases, we can help the compiler to derive the proper type.

As an example, we’ll create a find method that returns the first element satisfying a given predicate:

def find[A](xs: List[A], predicate: A => Boolean): Option[A] = {
  xs match {
    case Nil => None
    case head :: tail =>
      if (predicate(head)) Some(head) else find(tail, predicate)
  }
}

We can use this method to find the first even number.

find(List(1, 2, 3), x => x % 2 == 0)

The above code won’t compile because the compiler cannot figure out what type x is. We can resolve this problem is by defining the type as Int.

find(List(1, 2, 3), (x:Int) => x % 2 == 0) shouldBe Some(2)

This works, but we can help the compiler with type inference by moving the predicate function into the second argument list.

def find[A](xs: List[A])(predicate: A => Boolean): Option[A] = {
  xs match {
    case Nil => None
    case head :: tail =>
      if (predicate(head)) Some(head) else find(tail)(predicate)
  }
}

This tiny change will help the compiler resolve the type properly. After passing the list of Ints into the first argument list, the compiler will know that we are working with Ints and use this information to infer that the predicate should have type Int => Boolean.

find(List(1, 2, 3))(x => x % 2 == 0) shouldBe Some(2)

5. Flexibility

Currying and partial application make it possible to create smaller functions of differing behavior by applying some arguments to the curried function. Let’s make our sum function more generic by adding mapping function f: Int => Int, also known as the identity function, which will map both values before adding them together.

def sumF(f: Int => Int)(x: Int, y: Int): Int = f(x) + f(y)

Now we can apply the identity function to get the sum function.

val sum: (Int, Int) => Int = sumF(identity)
sum(1, 2) shouldBe 3

It’s also possible to create a squareSum function, which will add squares. We can do this by replacing the identity function.

val sumSquare: (Int, Int) => Int = sumF(x => x * x)
sumSquare(1, 2) shouldBe 5

We can go even further to create the increment and decrement functions based on the sum function.

val increment: Int => Int = sum.curried(1)
val decrement: Int => Int = sum.curried(-1)
increment(2) shouldBe 3
decrement(2) shouldBe 1

Curried functions are useful when one argument function is expected.

For example, let’s consider a situation in which we have a list of numbers, and we want to increment each of them using sum function:

val sum: (Int, Int) => Int = (x, y) => x + y
val numbers: List[Int] = List(1, 2, 3)
numbers.map(n => sum(1, n)) shouldBe List(2, 3, 4)

To make this code work, we need to explicitly pass into the sum function as the second argument. To avoid this, and improve our code readability, we can use currying:

val curriedSum: Int => Int => Int = x => y => x + y
val numbers: List[Int] = List(1, 2, 3)
numbers.map(curriedSum(1)) shouldBe List(2, 3, 4)

6. Conclusion

In this short tutorial, we learned about currying and partial application and how they differ.

Then, we saw how they can work together to make our code more flexible, readable and help compiler infer types.

As always, the full source code of the article is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments