In this document, we will understand what partially applied functions are and how they can be re-used to create other functions. We will also look at how partially applied functions are different from partial functions. Last but not the least we will look at function currying.
Partially Applied Functions
What are Partially Applied Functions
- Partially applied functions means that while applying the function, we do not pass in arguments for all of the parameters defined by the function, but only for some of them, leaving the remaining ones blank.
- What we get back is a new function whose parameter list only contains those parameters from the original function that were left blank.
- When a partially applied function is called, the arguments that have not been specified have to be specified as _
Example 1
def add(a: Int, b: Int) = a + b
val onePlusFive: Int = add(1, 5) // 6 val addToFour: Int => Int = add(4, _) //Returns a function Int => Int val addFourPlusTwo: Int = addToFour(2) // 6
Example 2: Creating a size filter on Strings
type Predicate = (Int, Int) => Boolean def sizeConstraint(p: Predicate, n: Int, text: String) = p(text.size, n)
val gte: Predicate = _ >= _ // gte: Predicate = <function2> val lte: Predicate = _ <= _ // lte: Predicate = <function2>
val minSize: (Int, String)=>Boolean = sizeConstraint(gte, _, _) // minSize: (Int, String) => Boolean = <function2> val maxSize: (Int, String)=>Boolean = sizeConstraint(lte, _, _) // maxSize: (Int, String) => Boolean = <function2>
minSize(5, "John") // res0: Boolean = true minSize(10, "John") // res1: Boolean = false
Example 3: Creating a generic function for sum/product on function applied to range of numbers.
e.g. sum of cubes, product of squares, factorial, etc
import scala.annotation.tailrec def operate(f:Int=>Int, combine:(Int,Int) => Int, identity: Int, start:Int, end:Int):Int = { @tailrec def functionLoop(current:Int, accumulator:Int):Int = if(current > end) accumulator else functionLoop(current+1, combine(f(current), accumulator)) functionLoop(start, identity) }
Now we will demonstrate how the above operate function can be re-used to create other partially applied functions
// Returns a function that can calculate sum // for a given range of integers after applying a function to each integer // Returns a function (Int => Int, Int, Int) => Int def sumF = operate(_:Int=>Int, (x:Int,y:Int) => x+y, 0, _:Int, _:Int)
// Returns a function that can calculate product // for a given range of integers after applying a function to each integer // Returns a function //(Int => Int, Int, Int) => Int def prodF = operate(_:Int=>Int, (x:Int,y:Int) => x*y, 1, _:Int, _:Int)
// Returns a function that can calculate sum of cubes, for a range of integers // Returns a function (Int, Int) => Int def sumCubes = sumF(x=>x*x*x, _:Int, _:Int )
// Returns a function that can calculate product of squares, for a range of integers // Returns a function (Int, Int) => Int def prodSquares = prodF(x => x*x, _:Int, _:Int)
// Returns a function that can be used to calculate factorial of a number // Returns a function Int => Int def factorial = prodF(x=>x, 1, _:Int)
So as we can see, we re-used the same function ‘operate‘ to create other functions by passing only some of the parameters. These new functions can now be used in the following way. (The consumer of these functions don’t know anything about the original ‘operate‘ function, neither they have to pass all the 5 parameters that operate require)
sumCubes(1, 5) // 225 prodSquares(2,4) // 576 factorial(5) //120
How is Partially Applied functions different from Partial Functions?
They are different from Partial functions in a way that partial functions are unary function that does not support every possible value that meets the input type.
However partial functions need to be passed all the parameters, whereas partially applied functions can be passed some of the parameters.
Currying
Currying means taking a function that takes multiple arguments and turning it into a chain of functions each taking one argument and returning the next function, until the last returns the result.
In pseudocode, this means that an expression like this
result = f(x)(y)(z)
Above function is mathematically same as something like this:
f1 = f(x) f2 = f1(y) result = f2(z)
Transformation of a normal function (that takes multiple arguments) to a curried function (that takes a chain of functions, each taking single argument), can be done using the curried method on functions.
Example 1: Creating curried functions from functions with multiple arguments.
Below are two addition functions, that take multiple arguments
val sum: (Int,Int)=>Int = _ + _ def add(x: Int, y: Int): Int = x + y
Creating a curried function automatically
def sumCurried: Int => (Int => Int) = sum.curried
Creating a curried function manually
def addCurried(x: Int)(y: Int): Int = x + y
Using curried functions, and normal functions both will yield same results
sum(1,5) // Result is 6 add(1,5) //Result is 6 sumCurried(1)(5) //Result is 6 addCurried(1)(5) //Result is 6
Example 2
Below is the example of a curried function that can be used to create sum of numbers, product of numbers, factorial, sum of cubes, etc
import scala.annotation.tailrec def functionOp(f:Int=>Int)(combine:(Int,Int) => Int)(identity: Int)(start:Int)(end:Int):Int = { @tailrec def functionLoop(current:Int, accumulator:Int):Int = if(current > end) accumulator else functionLoop(current+1, combine(f(current), accumulator)) functionLoop(start, identity) }
Passing just the first argument to above function, will create another curried function
def id = functionOp(x => x)_
//((Int, Int) => Int) => (Int => (Int => (Int => Int)))
def product = id((x,y) => x*y)(1)
//Int => (Int => Int)
def factorial(n:Int):Int = product(1)(n)
//Int => Int
def sumSquares = functionOp(x=>x*x) ((x,y) => x+y) (0)_
//Int => (Int => Int)
def prodSquares = functionOp(x=>x*x) ((x,y) => x*y) (1)_
//Int => (Int => Int)
Above functions can be used in following way
factorial(5) // 120 factorial(10) //3628800 sumSquares(1)(5) //55 prodSquares(1)(5) //14400
Example 3
Below is a curried function to check if mod(x,n) is true
def modN(n: Int)(x: Int):Boolean = ((x % n) == 0)
Above curried function can now be used to filter the list to get all elements that are divisible by 2, or all numbers divisible by 3
val nums = List(1, 2, 3, 4, 5, 6, 7, 8) nums filter modN(2) //Filter list to get all numbers divisible by 2 = List(2, 4, 6, 8) nums filter modN(3) //Filter list to get all numbers divisible by 3 = List(3, 6)