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

  1. modifies the value of a variable or state of a mutable object that can be accessed outside the function,

  2. 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

  3. 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

  1. it does not have side effects, and

  2. 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.