Functions
Let us next discuss how to use functions and collections together to work with data, to illustrate the functional programming style. As a basic example, it is quite concise to write
val l = Vector(1.0, 2.3, 3.0)
val sumOfSquares = l.map(v => v*v).sum
Indeed, contrast this with the imperative-style equivalent
val l = Vector(1.0, 2.3, 3.0)
var i = 0
var sumOfSquares = 0.0
while i < l.length do
sumOfSquares += l(i) * l(i)
i = i + 1
end while
The key idea in functional programming is that
we see computation as evaluation (application) of functions on data.
For example, above we first
apply the function v => v*v
to each element v
of the vector l
to get a new vector, namely Vector(1.0, 5.289999999999999, 9.0)
.
We then apply the method sum
to reduce this vector with
the sum function (x,y) => x+y
.
Higher-order functions.
Methods and functions that take as arguments or return other functions,
such as the map
method of the Vector
class used above,
are called higher-order functions.
Also note that, as we only apply functions to data and get new data,
we do not modify the original data: thus we do not use var
declarations
or mutable data structures like ArrayBuffers
. Rather, we use their
immutable equivalents, such as Vector
.
Before we take a closer look on the higher-order functions provided
in the collection classes, let us review how to write anonymous functions
such as v => v*v
above.
Anonymous functions
As our running example, consider the code
val l = Vector(3, 4, 2, 7, 8, 11)
val evens = l.filter(v => v % 2 == 0)
val sumOfEvens = evens.sum
The anonymous function v => v % 2 == 0
corresponds to the val
defined function
val isEven: Int => Boolean = v => (v % 2 == 0)
That is: it is a function from integers to Booleans,
returning true on an integer argument v
if and only if
(v % 2 == 0)
holds (i.e., when v
is even).
(To be pedantic, we should actually talk about predicates here as the range of the function is the set of Booleans.)
If we had defined this function explicitly, we could have written
val evens = l.filter(isEven)
instead of
val evens = l.filter(v => v % 2 == 0)
Type inference.
So how does Scala compiler know that
v => v % 2 == 0
is a function from integers to Booleans?
The compiler automatically infers this fact from the context
where the function is defined and used:
this anonymous function is defined in the parameter list of the filter
method of a value of type Vector[Int]
. Therefore, the function must
have one argument of the same (or sub-) type as the elements in
the Vector
and it must return a Boolean
value.
Underscore notation for anonymous functions. Let us also recall the underscore notation of anonymous functions. Namely, we can use the underscore-symbols (“_”) for denoting the parameters of the function and omit the parameter list. The first “_” means the first parameter, the second “_” the second parameter and so on. For example, the above code can be equivalently written as:
val l = Vector(3, 4, 2, 7, 8, 11)
val evens = l.filter(_ % 2 == 0)
val sumOfEvens = evens.sum
If some parameter is used more than once in the function body,
then we cannot use the “_” notation.
For instance, “val squares = l.map(v => v*v)
” for computing the squares
of values in l
cannot be rewritten as “val squares = l.map(_ * _)
”:
the “_ * _
” defines a function “(v1,v2) => v1*v2
” with two parameters,
not the one-parameter function we intended.
Note
Tech corner: Functions as objects
How is it possible to pass functions, such as “v => v % 2 == 0
” above,
as arguments to other functions?
After all, functions do not only involve data, but also program code!
Well, in fact we can achieve the same thing by using objects only.
As an example, take our anonymous function code “v => v % 2 == 0
” above.
Instead of it, we can prepare a separate class
class Anonf1 extends Function1[Int,Boolean]:
def apply(v: Int): Boolean = (v % 2 == 0)
end Anonf1
that implements the function’s code in the body of the apply
method.
Similarly, the command “val evens = l.filter(v => v % 2 == 0)
”
is rewritten into:
val anonf1 = new Anonf1()
val evens = l.filter(anonf1)
The filter
-method then calls the apply
-method of
the anonf1
object when it evaluates the value of the function for
some element e
with anonf1(e)
(recall that the apply
method has a special treatment: we do not have to write anonf1.apply(e)
but can use the abbreviated form anonf1(e)
).
That is, instead of program code, we pass objects that implement the code.
Analogous translations happen automatically in the Scala compiler when it sees our anonymous function definitions.
Let us take a closer look at the
Function1 class for functions with one parameter (similar classes are provided for functions taking no parameters or multiple parameters).
This class provides a compose
method defined as:
/** Composes two instances of Function1 in a new Function1, with this function applied last.
*
* @tparam A the type to which function `g` can be applied
* @param g a function A => T1
* @return a new function `f` such that `f(x) == apply(g(x))`
*/
@annotation.unspecialized def compose[A](g: A => T1): A => R = { x => apply(g(x)) }
This allows us to define functions and then compose them. For instance:
scala> val f: Int => Boolean = {v => v % 2 == 0 }
f: Int => Boolean = <function1>
scala> val g: Int => Int = {v => v+1}
g: Int => Int = <function1>
scala> val h = f.compose(g)
h: Int => Boolean = <function1>
scala> h(3)
res: Boolean = true
Anonymous functions with pattern matching
There is also another, very convenient way to define anonymous functions:
pattern matching with case-expressions.
In fact, thanks to Scala 3’s option-less pattern matching, we can often skip the case
statement and extract variables from a tuple directly.
For example, we can select the indices at which a list contains an even number as follows:
scala> val l = List(1,6,3,7,8,4,5,3)
l: List[Int] = List(1, 6, 3, 7, 8, 4, 5, 3)
scala> val lWithIndices = l.zipWithIndex
lWithIndices: List[(Int, Int)] = List((1,0), (6,1), (3,2), (7,3), (8,4), (4,5), (5,6), (3,7))
scala> val evensWithIndices = lWithIndices.filter((num, _) => num % 2 == 0)
evensWithIndices: List[(Int, Int)] = List((6,1), (8,4), (4,5))
scala> val result = evensWithIndices.map(_._2)
result: List[Int] = List(1, 4, 5)
Here the expression (num, _) => num % 2 == 0
defines an anonymous
function such that, given a pair as an argument,
it returns true if the first element in the pair is even.
The underscore in (num, _)
indicates that the second element in the pair
is not interesting.
Further, the anonymous function _._2
(recall that a pair has methods ._1
and ._2
returning the first and second element in it, respectively)
in the last map
call above could also have been written
as val result = evensWithIndices.map((_,index) => index)
.
The case
-part of an expression needs to be included if one wants to pattern match nested tuples, however. Further, case
-expressions can also have multiple cases,
of which the first matching case is selected,
and inspect the type of the argument object:
scala> val l = List("recursion", 2, 2.9)
val l: List[Matchable] = List(recursion, 2, 2.9)
scala> l.foreach({case s:String => {println("a string of length "+s.length)}
case n:Int => println("a number")
case _ => println("an unidentified object") })
a string of length 9
a number
an unidentified object
Here we do different things depending of what types we encounter in the list.
We will return to pattern matching and cases in A second implementation with case classes. Alternatively you can study, for example, Section 7 of Scala by Example or Chapter 15 of Programming in Scala, First Edition for more on case classes and pattern matching.
Methods as functions
In super-precise terms, methods in classes are not functions but methods. Therefore, we cannot use methods directly like functions, for example, we cannot pass them as parameters to other functions:
scala> class A(val x: Int):
def isEven: Boolean = (x%2==0)
override def toString = "x="+x
end A
// defined class A
scala> val l = List(new A(1), new A(3), new A(8))
val l: List[A] = List(x=1, x=3, x=8)
scala> l.filter(A.isEven)
-- [E008] Not Found Error: -----------------------------------------------------
1 |l.filter(A.isEven())
| ^^^^^^^^
| value isEven is not a member of object A
To use methods as functions, however, we can define a very simple anonymous function that then calls the method:
scala> l.filter(x => x.isEven)
val res: List[A] = List(x=8)
Scala offers an easy syntax for treating methods defined in objects as functions: we simply add ” _” after the method name to translate it into a function object:
scala> object areaCalculator:
def circle(radius: Double) = scala.math.Pi * radius * radius
def square(side: Double) = side * side
def rectangle(side1: Double, side2: Double) = side1 * side2
def triangle(base: Double, height: Double) = base * height * 0.5
end areaCalculator
// defined object areaCalculator
scala> val circleAreaFunction = areaCalculator.circle _
circleAreaFunction: Double => Double = <function1>
scala> val circleRadiuses = List(2.0, 3.9, 1.2)
radiuses: List[Double] = List(2.0, 3.9, 1.2)
scala> val circleAreas = circleRadiuses.map(circleAreaFunction)
circleAreas: List[Double] = List(12.566370614359172, 47.783624261100755, 4.523893421169302)
This ” _”-construction is in fact used automatically by the compiler and thus we can simply write:
scala> val circleAreas = circleRadiuses.map(areaCalculator.circle)
circleAreas: List[Double] = List(12.566370614359172, 47.783624261100755, 4.523893421169302)
A function obtained in this way can naturally access the elements of its enclosing object. For instance, consider a simple class to help in formatting output:
class indentedWriter(s: java.io.PrintStream, prefix: String = ""):
private var level = 0
def println(obj: Any) = s.println(prefix + (" "*level) + obj)
def push : Unit = {level += 2 }
def pop : Unit = {level = 0 max level-2 }
end indentedWriter
Now the code
val out = new indentedWriter(Console.out, "> ")
val values = Vector(1,2,4)
out.println("The values are:")
out.push
values.foreach(out.println)
out.pop
out.println("Their sum is "+values.sum)
outputs
> The values are:
> 1
> 2
> 4
> Their sum is 7
Side effects and pure functions
We say that a function or a method has side effects if it, in addition to providing the return value, does at least one of the following
modifies the value of a variable or state of a mutable object that can be accessed outside the function,
causes some other observable interaction with its environment, for instance writes data to the file system, asks for input from the user, and so forth, or
calls some other function that has side effects.
Remark. Consumption of computational resources such as time and memory when executing a function is not considered as a side effect.
As an example, let us consider the following (not so elegant or efficient) function that tests whether a positive integer is a prime number:
def isPrimeSlow(n: Int): Boolean =
require(n > 0)
var divisor = 2
while divisor < n do
if n % divisor == 0 then return false
divisor += 1
end while
return true
end isPrimeSlow
Even though the function modifies the value of the variable divisor
during its execution, the function does not have side effects because
the variable is not visible outside the function definition.
Pure functions
A function is pure if
it does not have side effects, and
its evaluation with the same arguments always gives the same return value.
For instance, our primality checking function above is pure but the function
def getDate: String =
val d = new java.util.Date() // get the current date
d.toString
end getDate
is not pure even though it does not have side effects.