1. Overview
Scala is a type-safe JVM language that incorporates both object-oriented and functional programming into an extremely concise, high-level, and expressive language. Scala provides frameworks like Apache Spark for data processing, as well as tools to scale programs based on the required needs. All these powerful features of Scala make it a very practical and sought-after language.
In this tutorial, we’ll learn about functional programming concepts with Scala as a programming language.
2. What Is Functional Programming?
Functional programming is a programming paradigm that uses functions as the central building block of programs. In functional programming, we strive to use pure functions and immutable values.
2.1. Immutability
Immutability means programming using constants, which means the value or state of variables can’t be changed. The same goes with objects: We can create a new object, but we cannot modify the existing object’s state. Thus, immutable objects are more thread-safe than mutable objects. We’ll study how Scala enforces immutability in a later section.
2.2. Pure Functions
A pure function has two key properties:
- It always returns the same value for the same inputs.
- It has no side effects. A function with no side effects does nothing other than simply return a result. Any function that interacts with the state of the program can cause side effects. The state of the program can be mutable objects, global variables, or I/O operations, for example.
We’ll see more about pure functions in Scala in the next section.
3. How Is Scala Functional?
Scala is a functional language in the sense that every function is a value. The fundamental concept of functional Scala is functions act as first-class citizens in Scala.
There are various functional constructs that we can create in object-oriented programmings, like functions that take or return other functions and methods that are defined inside one another. But in functional programming, these occur naturally and arise quite often. In object-oriented programming, we somewhat rarely encounter these phenomena.
3.1. Functions as First-Class Citizens
When we treat a function as a value, we consider it a first-class function.
In general, a first-class function can be:
- assigned to a variable
- passed as an argument to other functions
- returned as a value from other functions
Scala treats all functions as first-class functions by default.
3.2. Higher-Order Functions
The concept of a higher-order function (HOF) is closely associated with that of a function as a first-class citizen.
A higher-order function has at least one of the following properties:
- Takes one or more functions as parameters
- Returns a function as a result
Basically, by using HOFs, we can work with functions as we work with any other kinds of values.
Let’s understand this by creating a function that takes a function as a parameter:
def calcAnything(number: Int, calcFunction: Int => Int): Int = calcFunction(number)
def calcSquare(num: Int): Int = num * num
def calcCube(num: Int): Int = num * num * num
val squareCalculated = calcAnything(2, calcSquare)
assert(squareCalculated == 4)
val cubeCalculated = .calcAnything(3, calcCube)
assert(cubeCalculated == 27)
Let’s see one more example of a function returning another function as a result:
def performAddition(x: Int, y: Int): Int = x + y
def performSubtraction(x: Int, y: Int): Int = x - y
def performMultiplication(x: Int, y: Int): Int = x * y
def performArithmeticOperation(num1: Int, num2: Int, operation: String): Int = {
operation match {
case "addition" => performAddition(num1, num2)
case "subtraction" => performSubtraction(num1, num2)
case "multiplication" => performMultiplication(num1, num2)
case _ => -1
}
}
val additionResult = performArithmeticOperation(2, 4, "addition")
assert(additionResult == 6)
val subtractionResult = performArithmeticOperation(10, 6, "subtraction")
assert(subtractionResult == 4)
val multiplicationResult = performArithmeticOperation(8, 5, "multiplication")
assert(multiplicationResult == 40)
Higher-order functions also allow us to create function composition, lambda functions, or anonymous functions.
3.3. Anonymous Functions
When using HOFs, it’s often convenient to pass anonymous functions or function literals as parameters to these functions, rather than having to supply some existing named function. A function that has no name but has a body, input parameters, and return type (optional) is an anonymous function. We also refer to this as a Function Literal or Lambda Expression.
Some classical HOFs that take an anonymous function as an argument are the map, filter, and fold functions in Scala’s standard collections library.
3.4. Closures
A closure is like any other function in Scala. It may be a named function or an anonymous function, or it may be pure or impure, but closure is primarily a function.
We might be wondering then, what’s the difference between a function and a closure? Let’s understand this with a simple example:
val rate = 0.10
val time = 2
def calcSimpleInterest(principal: Double): Double = {
(principal * rate * time) / 100
}
val simpleInterest = 20
assert(calcSimpleInterest(10000) == simpleInterest)
In this example, we’ve defined a function calcSimpleInterest(), which is using two free variables, rate and time. Hence, this is a closure.
A free variable is a variable that is used in the function but is neither a local variable nor a formal parameter to the function. The only difference between a function and closure is that a closure uses one or more free variables.
3.5. Currying
Currying means transforming a function that takes multiple arguments into a chain of calls to functions, each of which takes one argument. Each function returns another function that takes the subsequent argument.
This results in an expression that looks like this:
result = f(x)(y)(z)
Let’s understand this by creating a function with two arguments and converting it to a curried function:
val multiplication: (Int, Int) => Int = (x, y) => x * y
val curriedMultiplication: Int => Int => Int = x => y => x * y
val multiplicationResult = multiplication(3, 5)
val curriedMultiplicationResult = curriedMultiplication(3)(5)
assert(multiplicationResult == curriedMultiplicationResult)
We can also use a special curried method with multiple arguments to perform the same operation:
val conciseCurriedMultiplication: Int => Int => Int = multiplication.curried
val conciseCurriedMultiplicationResult = conciseCurriedMultiplication(3)(5)
assert(multiplicationResult == conciseCurriedMultiplicationResult)
Scala has a separate syntax to ease currying by creating multiple argument lists:
def addition(x: Int, y: Int): Int = x + y
def curriedAddition(x: Int)(y: Int): Int = x + y
val additionResult = addition(8, 4)
val conciseCurriedAdditionResult = curriedAddition(8)(4)
assert(additionResult == conciseCurriedAdditionResult)
3.6. Partially Applied Functions
In functional programming, a call to a function that has parameters can also be stated as “applying the function” to the parameters. When a function is called with all the required parameters, then the function is fully applied to all of its parameters. But when only a subset of the parameters is passed to the function, then the function is called a partially applied function. Once the required initial parameters are provided to the partially applied function, a new function is returned with the rest of the arguments that need to be passed.
To understand this, let’s take an example where we define a method to calculate selling price after discount:
def calculateSellingPrice(discount: Double, productPrice: Double): Double = {
(1 - discount/100) * productPrice
}
val discountApplied = calculateSellingPrice(25, _)
val sellingPrice = discountApplied(1000)
assert(sellingPrice == 750)
The method calculateSellingPrice() takes two arguments — the first is the discount to be applied, and the second is the product price. Consider a scenario where the shopkeeper has set a discount of 25% for all the products. In this case, we can simply pass the discount parameter to the function and return a new function with productPrice as a missing parameter. Then, we can use discountApplied for all the products without being bothered about the value of the discount.
4. Conclusion
In this article, we introduced functional programming in Scala and explained how exactly Scala is functional and why we might use it.
Scala provides us with enough object-oriented programming that we’ll feel like we’re on familiar ground. At the same time, it’s an excellent way to get to know functional programming at our own pace.
As usual, we can find code snippets over on GitHub.