Partially Applied Functions

Think about functions and ask this question to yourself.
What is a function?
Whatever definition you accept, you can always break a function into two parts.

  1. A fixed part (function body)
  2. A variable part (function arguments)

We keep the fixed part into the body of a function and pull the variable parts outside the body as a parameter list. The purpose is to create a generalized solution. Let's look at a couple of simple examples.

    def div(x:Double, y:Double):Double  = x/y
    def add(x:Double, y:Double):Double  = x + y              

Above functions are generic solutions for a pair of numbers. The first one is a division and the second one is an addition. These are small and simple functions, but they also have a fixed part (function body) and a variable part (argument list). Have you ever thought that fixing some of the variable parts (arguments) can give you an entirely different solution?
Let's see an examples.

    def inv(y:Double):Double = 1.0/y                 

The above code is an inverse function. The division is a general solution, and the inverse is a particular case of a division operation. Isn't it? You fix the first variable to one, and the division becomes an Inverse operation. Similarly, you set the first variable to one, and the addition becomes an increment operation. Inverse and increment are entirely new solutions.
Let's say. You have a function to charge a customer for an online purchase. It takes two parameters. The payment method and the amount. You fix the payment method to the credit card, and it becomes a new function. PayByCreditCard. Similarly, you can create other specialized cases like PayByWallet. The idea is similar to the one shown in below figure.

Partially Applied Functions
Fig.1- Shows the idea behind Partially Applied Functions.

Take a generic function, fix the value for some parameters and you get a brand new specialized function. We have a standard terminology for this whole idea. That is what we call the partially applied functions. The notion of partially applied function is powerful, and hence Scala offers a syntax to implement the partial application of parameters. The implementation is extremely simple.

Partially Applied Function Syntax

Let me take the division function and use it to demonstrate the syntax.

    def div(x:Double, y:Double):Double  = x/y
    //div: (x: Double, y: Double)Double
    div(1, _:Double)
    //res2: Double => Double = <function1>
    val inverse = div(1, _:Int)
    //res3: Double = 0.1                

The first line in the above code is my division function. I said, fix the first parameter to 1, and you can create a brand new function for the inverse. The second line does the same thing. We fixed the first parameter to one and gave a placeholder for the second parameter.
In fact, we applied the div function to its first argument and left the second argument as unapplied. That's why we call it a partially applied function.
We have a small limitation in this syntax. We must specify the data type for the unapplied parameters. The type inference doesn't work in this case.
If you look at the output of the partially applied function, Scala returned a new function. Strange. Isn't it. If I look at the implementation of my div function, It doesn't return a function. It simply returns a double. But in case of partially applied functions, the Scala compiler will do a couple of things internally, and instead of returning a double, it will return a new function that takes the remaining unapplied parameters.
You can hold the returned function into a val. That's what we do in the next line.
The inverse is now a new function that takes a single argument because we didn't apply one argument earlier. You can call inverse as shown at the last line. Amazing. Isn't it.

Partial application in Higer Order functions

In functional programming languages, the notion of partially applied function is much more powerful because we can have a parameter that itself is a piece of code (a function literal). You can achieve much more by hooking up a new algorithm to a generalized Higher Order Function.
Let's see an example. I have got a requirement to calculate different types of sums for given bounds. Let's assume the lower bound is 1 and the upper bound is 5. And the requirement is to calculate three types of sums as listed below.

  1. A simple sum.
    1 + 2 + 3 + 4 + 5 = 15
  2. A sum of squares.
    1*1 + 2*2 + 3*3 + 4*4 + 5*5 = 55
  3. A sum of cubes.
    1*1*1 + 2*2*2 + 3*3*3 + 4*4*4 + 5*5*5 = 255

Do you want to create three different functions for these three requirements? I don't want to do that because I am expecting to get some more similar requirements. So I decided to create a higher order function. The code is shown below.

    def sumOfX(f: Int=> Int, a: Int, b: Int):Int = 
        if (a>b) 0 else f(a) + sumOfX(f, a+1, b)               

The first parameter is the logic. The other two parameters are the lower and upper bounds. I can fulfill all the three requirements using the higher-order function SumOfX. The code is shown below.

    sumOfX(x=>x, 1,5)
    sumOfX(x=>x*x, 1,5)
    sumOfX(x=>x*x*x, 1,5)           

To calculate a simple sum, I can use an identity function. To calculate a sum of squares, I can use a squaring function. Similarly, to calculate a sum of cubes, I can use a cube function. I can also fulfill some future requirements. For example, to calculate a sum of factorials, all I need to do is to pass a factorial function.
Great. However, the example shows a use of a Higher Order function. There is nothing to do here with partially applied functions. Now, you might be wondering, why do we need a partially applied function? I mean, it's a nice idea. Take a generic function, fix some of the parameters and create a brand new function. But why the hell I need to do that? If at all I need to calculate a sum of cubes, I will simply use the SumOfX as shown above. Why do I take the pain to create a partially applied function and then use it to calculate the sum of cubes?
There is a difference in the type. Let me explain. Let's create a partially applied function.

    val sumOfCubes = sumOfX(x=>x*x*x, _:Int, _:Int)           

Now the sumOfCubes is a function that takes two parameters. But sumOfX was taking three parameters. When you have a downstream function that expects sumOfCubes, you cannot use sumOfX. Let's take an example.

    def applySums(f: (Int, Int) => Int) = 
        println("I am applying the sum of 1 to 5 and answer is = " + f(1,5))           

The applySums takes a function of type (Int, Int) => Int. If you try passing sumOfX, you will get a type mismatch error. However, you can use a partially applied function to generate a function of type (Int, Int) => Int. Let's try it.

    applySums(sumOfX(x=>x*x*x, _:Int,_:Int))
    applySums(sumOfX(x=>x*x, _:Int,_:Int))          

So, the partially applied function is a technique to generate a brand new function. And the generated function comes with a brand new type. You mostly need it to pass it to the downstream functions.
Continue reading for more on Scala functions.

Read More

Basics of Scala functions | Function Literals in Scala | Function values | Local Functions | Variable length argument | Default values and named arguments | Scala Placeholder syntax | Higher Order functions | Partially applied functions | Function currying

By Prashant Pandey -

You will also like:

Kafka Core Concepts

Learn Apache Kafka core concepts and build a solid foundation on Apache Kafka.

Learning Journal

Scala Function Basics

Start learning Scala functions from the basics and become an expert.

Learning Journal

Referential Transparency

Referential Transparency is an easy method to verify the purity of a function.

Learning Journal

Lazy Evaluations

Evaluate the expression now vs evaluate it for the first use. Strict vs Lazy?

Learning Journal

Higher Order functions

Scala allows you to create Higher Order functions as first class citizens.

Learning Journal