Scala Higher-Order Functions
In the Scala programming language, higher-order functions, in short, are functions that either take a function as a parameter or return a function.
In Scala, functions are first-class values. It means that functions are treated like any other value or data type. Functions can be assigned to variables, passed as arguments to other functions and returned as results from other functions. This flexibility allows functions to be used in a wide range of contexts and makes functional programming constructs like higher-order functions, function literals (anonymous functions), and function composition possible in Scala.
Functions as parameter
Suppose we have a list of integers, and we would like to create a new list by adding 3 to each element in the original list. We can achieve this by using the addThreeToEach
function in the following code.
// Define a higher-order function that takes a function as an argument
private def addThreeToEach(numbers: List[Int], f: Int => Int): List[Int] = {
numbers.map(f)
}
// Define a function that adds 3 to a number
private def addThree(n: Int): Int = {
n + 3
}
@main def run(): Unit =
val numbers = List(1, 2, 3, 4, 5)
// Use the higher-order function to add 3 to each number in the list
val result = addThreeToEach(numbers, addThree)
println(result) // Output: List(4, 5, 6, 7, 8)
The higher-order function addThreeToEach
is a function that takes a list of integers and a function parameter, and returns each element incremented by 3.
Without defining the addThree
method, we can also pass the function body directly to the addThreeToEach
function when calling it:
// Define a higher-order function that takes a function as an argument
private def addThreeToEach(numbers: List[Int], f: Int => Int): List[Int] = {
numbers.map(f)
}
@main def run(): Unit =
val numbers = List(1, 2, 3, 4, 5)
// Use the higher-order function to add 3 to each number in the list
val result = addThreeToEach(numbers, n => n + 3)
println(result) // Output: List(4, 5, 6, 7, 8)
Furthermore, we don’t need to define a higher-order function explicitly in the code; we can define it anonymously while calling it:
@main def run(): Unit =
val numbers = List(1, 2, 3, 4, 5)
// The higher-order function definition when printing the result
println(((list:List[Int]) => list.map(n => n + 3))(numbers))
// Output: List(4, 5, 6, 7, 8)
Functions that return functions
In particular scenarios, it can be necessary to generate a function. Here’s a demonstration of a method that yields a function:
// Define a function that returns another function
private def bonusCalculator(performancePoint: Int): (Double) => Double = (salary:Double) => {
performancePoint match
case 1 => 0
case 2 => 0.3 * salary * 0.2
case 3 => 0.3 * salary * 0.5
case 4 => 0.3 * salary * 0.8
case 5 => 0.3 * salary
}
@main def run(): Unit =
// Calculate the bonus for a staff whose salary is 1000
// and performance point is 4
println( bonusCalculator(4)(1000) )
Notice the return type of bonusCalculator: (Double) => Double
. This means that the anonymous function returned by bonusCalculator
takes a Double
as input and returns a Double
as output. The bonusCalculator
function returns the calculator function based on the performancePoint
parameter. In the function body, performancePoint
is checked using pattern matching, and the corresponding function definition is returned from the bonusCalculator
function.
In the println statement, bonusCalculator(4)
returns the associated (Double) => Double
function based on the parameter 4 (performancePoint). Then, with the (1000)
code, the parameter of the returned function is applied, and the returned function is invoked in this way.
Function Composition
Function composition is a technique where we can chain together multiple functions, applying the result of one function as the parameter of another. It allows us to create a new function that combines the behavior of the individual functions in a chain.
Below, you can see an example:
// Define a higher-order function that takes a function f and returns a new function
private def compose[A, B, C](f: B => C, g: A => B): A => C = {
(x: A) => f(g(x))
}
// Define two basic functions
private def increment(x: Int): Int = x + 1
private def square(x: Int): Int = x * x
// Use the higher-order function to compose the two functions
private val composedFunction: Int => Int = compose(square, increment)
@main def run(): Unit =
// Test the composed function
val result: Int = composedFunction(4)
println(result) // Output: 16 (square(increment(4)) = square(5) = 25)
We define 2 simple functions. The increment
function adds 1 to its input, while the square
function squares its input.
At first glance, the compose
function may seem difficult and complicated to understand, but when you examine it step by step, you will realize that it is not as challenging as it initially appears. It combines increment
and square
into a new function that first increments its input and then squares the result.
The (f: B => C, g: A => B)
indicates the two functions used as parameters in the compose
method. In the g
function, you input A
and it returns B
. In the f
function, you input B
, which is the output of g
, and it returns C
.
The A => C
indicates that the compose
function returns a function definition. When you input A
(the parameter of the g
function) into this returned function, it yields C
, which is the output of the f
function.
In the function body, f(g(x))
implies the return of a function that first calls the g
function and then calls the f
function with the result of the g
function.
The composedFunction
combines increment
and square
into a new function that first increments its input and then squares the result.
Conclusion
In this tutorial, we implemented some examples of higher-order functions in Scala, showcasing their power and versatility. As demonstrated, higher-order functions enable us to produce readable, concise code while promoting code reuse and modularity. By leveraging higher-order functions effectively, developers can enhance their Scala programming skills and create more maintainable and flexible codebases. However, it’s important to be mindful of potential challenges and pitfalls when working with higher-order functions. We encourage further exploration of this topic through additional resources and advanced topics to deepen your understanding and proficiency in Scala programming.