1. Introduction
Functions and methods in Scala represent similar concepts, but there are significant differences in how we use them.
In this tutorial, we’ll have a look at their definitions and usage differences.
2. Functions
A function is a callable unit of code that can take a single parameter, a list of parameters, or no parameters at all. A function can execute one or many statements and can return a value, a list of values, or no values at all.
We can reuse this unit of code so that we don’t need to repeat it.
2.1. Anonymous Function
An anonymous function is one without a name or an explicit handle. This can be useful when we need to provide a piece of code or action as a parameter to another function.
Let’s see an example:
(number: Int) => number + 1
On the left side, in parentheses, we define function parameters. After the => sign, we define an expression or list of statements.
The parameter list can be empty (), or we can define as many parameters as we need:
() => scala.util.Random.nextInt
(x: Int, y: Int) => (x + 1, y + 1)
To define more than one statement, we must enclose them in curly braces. The result of a final expression in a block of code is a function’s return value.
Scala has a return keyword, but it’s rarely used:
(number: Int) => {
println("We are in a function")
number + 1
}
In the example above, we defined a function that takes one parameter named number. The function body has two expressions. The last expression increases the parameter value by one and returns the result.
The type of result is inferred automatically — argument number is of type Int, and after adding 1, the result is also an Int.
2.2. Named Function
Everything is an object in Scala, so we can assign a function to a value:
val inc = (number: Int) => number + 1
Value inc now contains a function. We can use this value everywhere we need to call the unit of code defined in function:
scala> println(inc(10))
11
Thus, inc is considered a named function.
There is some “magic” behind-the-scenes that Scala does to allow the assignment of a function to a variable. All functions in Scala are special objects of type Function1 or Function2 or FunctionN, where N is the number of input arguments to the function.
The compiler will box the function code we provide into the appropriate object automatically. This object contains a method apply() that can be called to execute our function.
Furthermore, parentheses are “syntactic sugar” for calling a function’s apply() method. So, these two statements are equivalent:
scala> println(inc(10))
11
scala> println(inc.apply(10))
11
2.3. Closure
A function whose return value depends on some state outside its context is called a closure. A closure is a pure functional mechanism to emulate objects, as it allows for a developer to pass around a function closed over some context.
To illustrate this, let’s assume we have a plotter functionality:
def plot(f: Double => Double) = { // Some implementation }
Our function plot takes any function f(x) and visualizes it.
Next, here’s a definition of a linear equation in two dimensions:
val lines: (Double, Double, Double) => Double = (a,b,x) => a*x + b
This equation describes all possible lines in 2D. To get one particular line – for example, for plotting – we have to provide a and b coefficients. Only the x has to be a variable part of the linear equation.
We can’t just use the lines function as-is with our plotter functionality. Our plot function takes – as its argument – another function that has only one parameter of type Double and a Double type as a return value. A closure can help with that:
val line: (Double, Double) => Double => Double = (a,b) => x => lines(a,b,x)
The line function is a higher-order function — it returns another function of type Double => Double, which we can use in our plot function. We only need to provide two coefficients, and we’ll receive a function closed over these two parameters:
val a45DegreeLine = line(1,0)
Function a45DegreeLine() takes the value of an X coordinate and returns the corresponding Y coordinate, defining a line that goes through the center of these coordinates at 45 degrees.
Now, we can use our plot functionality to draw a line:
plot(a45DegreeLine())
3. Methods
Methods are essentially functions that are parts of a class structure, can be overridden, and use a different syntax. Scala doesn’t allow us to define an anonymous method.
We have to use a special keyword def for method definition:
def inc(number: Int): Int = number + 1
We can define a method’s list of parameters in parentheses after its name. After the parameters list, we can provide a return type for the method with the leading colon sign.
We then define a method’s body after the equals sign. The compiler allows us to skip the curly braces if there is only one statement of code.
Method definition allows us to skip parentheses completely for methods without parameters. To demonstrate this, let’s provide a method and a function with the same implementation:
def randomIntMethod(): Int = scala.util.Random.nextInt
val randomIntFunction = () => scala.util.Random.nextInt
Whenever we write a method name, the compiler will consider it as a call to this method. In contrast, a function name without parentheses is just a variable that holds a reference to a function object:
scala> println(randomIntMethod)
1811098264
scala> println(randomIntFunction)
$line12.$read$$iw$$iw..$$iw$$iw$$$Lambda$4008/0x00000008015cac40@767ee25d
scala> println(randomIntFunction())
1292055875
Thus, if we need to convert a method into a function, we can use the underscore:
val incFunction = inc _
Variable incFunction contains a function, and we can use it as other functions we defined:
scala> println(incFunction(32))
33
There is no corresponding way to convert a function into a method.
3.1. Nested Methods
We can define a method inside another method in Scala:
def outerMethod() = {
def innerMethod() = {
// inner method's statements here
}
// outer method's statements
innerMethod()
}
We can use the nesting feature in a situation when some method is tightly coupled with the context of another method. The code looks more organized when we use nesting.
Let’s see a common usage of a nested method via a recursive implementation of the factorial calculation:
import scala.annotation.tailrec
def factorial(num: Int): Int = {
@tailrec
def fact(num: Int, acc: Int): Int = {
if (num == 0)
acc
else
fact(num - 1, acc * num)
}
fact(num, 1)
}
The recursive implementation needs an extra method parameter to accumulate a temporal result between recursive calls, so we can use a decorative outer method with a simple interface to hide the implementation.
3.2. Parameterization
In Scala, we can parameterize a method by type. Parameterization allows us to create a generic method with the reusable code:
def pop[T](seq: Seq[T]): T = seq.head
As we can see, the type parameter was provided in square brackets during the method declaration.
Now we’re able to use method pop for any type T we have and get the head of a sequence provided as a function parameter.
Simply put, we’ve generalized the pop function:
scala> val strings = Seq("a", "b", "c")
scala> val first = pop(strings)
first: String = a
scala> val ints = Seq(10, 3, 11, 22, 10)
scala> val second = pop(ints)
second: Int = 10
In this case, the compiler will infer the type, so corresponding values will be of the appropriate type. Value first will be of type String, and value second will be of type Int.
3.3. Extension Method
Scala’s implicit feature allows us to provide additional functionality to any existing type.
We just need to define a new type with methods and to provide an implicit conversion from the new type into an existing type:
implicit class IntExtension(val value: Int) extends AnyVal {
def isOdd = value % 2 == 0
}
Here, we defined a so-called value class. A value class is just a wrapper around any value of a particular type. In our example, the type of value is Int.
A value class is a class with exactly one parameter and only methods inside its body. We extend it from AnyVal type, and unnecessary object allocation will be avoided.
To define an implicit conversion, we have to mark a class as implicit. This allows all methods inside a class to become implicitly accessible.
When we import such a class into the current context, we can use all methods defined inside this class, as they are defined in a value type itself:
scala> 10.isOdd
res1: Boolean = true
scala> 11.isOdd
res2: Boolean = false
Int value 10 doesn’t have an explicit method isOdd, but IntExtension class does. The compiler will search for an implicit conversion from Int to IntExtension. Our value class provides such a conversion. The compiler will use it to resolve the call to the function isOdd with the necessary parameters.
4. By-Name Parameters
Until now, we’ve used “by-value” parameters. In other words, any parameter inside our function or method is evaluated before we can access that value:
def byValue(num: Int) = {
println(s"First access: $num. Second access: $num")
}
scala> byValue(scala.util.Random.nextInt)
First access: 1705836657. Second access: 1705836657
We provided a function scala.util.Random.nextInt as a parameter to the call of our byValue function. As expected, the value of the parameter is evaluated before we use it in a println function. Both accesses to the parameter provide the same value.
Scala also supports “by-name” parameters. “By-name” parameter is evaluated every time we access them inside a function.
To define a “by-name” parameter, we have to prefix its type with => (arrow sign):
def byName(num: => Int) = {
println(s"First access: $num. Second access: $num")
}
scala> byName(scala.util.Random.nextInt)
First access: 1349966645. Second access: 236297674
The call to the function is the same, but the result is slightly different. Each time we access the parameter provided “by-name”, we evaluate the parameter’s value again. That’s why each access got different results.
5. Conclusion
In this tutorial, we showed how to define functions and methods in Scala, and what is the main differences and commons in their usage. We showed closure usage and learned how to provide an extension method for existing types.
As always, examples can be found over on GitHub.