Lazy evaluation

Writing efficient code is an important part of software engineering. A lot of times we will see cases where an expression is expensive to evaluate due to different possible reasons—database access, complex calculations, and so on. There are cases where we might even be able to exit the application without even evaluating these expensive expressions. This is where lazy evaluation becomes helpful.

Note

Lazy evaluation makes sure that an expression is evaluated only once when it is actually needed.

Scala supports lazy evaluation in a couple of flavors: lazy variables and by name parameters. We have already seen both in this book. The former we saw when we looked at creational design patterns in Chapter 6, Creational Design Patterns, and more specifically, lazy initialization. We saw the latter at a few places, but we encountered it for the first time in Chapter 8, Behavioral Design Patterns – Part 1, where we showed how to implement the command design pattern in a way that is closer to how Scala does.

There is an important difference between lazy variables and by-name parameters. The lazy variables will be calculated only once, whereas the by-name parameters would be calculated every time they are referred to in a method. There is a really simple trick we will show here that will fix this issue.

Evaluating by name parameters only once

Let's imagine that we have an application that takes data about people from a database. The reading operation is something that is expensive, and it is a good candidate for lazy evaluation. For this example, we will simply simulate reading from the database. First of all, our model will be as simple as:

case class Person(name: String, age: Int)

Let's now create a companion object that will have a method that simulates getting the data about people from a database:

object Person {
  
  def getFromDatabase(): List[Person] = {
    // simulate we're getting people from database by sleeping
    System.out.println("Retrieving people...")
    Thread.sleep(3000)
    List(
      Person("Ivan", 26),
      Person("Maria", 26),
      Person("John", 25)
    )
  }
}

The preceding code simply makes the current thread sleep for 3 seconds and returns a static result. Calling the thread multiple times will make our application slow, so we should consider lazy evaluation. Let's now add the following method to our companion object:

def printPeopleBad(people: => List[Person]): Unit = {
  System.out.println(s"Print first time: ${people}")
  System.out.println(s"Print second time: ${people}")
}

As you can see, we simply printed the list of data about people twice. And we accessed the by-name parameter twice. This is bad because it will evaluate the function twice and we will have to wait for twice the time. Let's write another version that will fix this issue:

def printPeopleGood(people: => List[Person]): Unit = {
  lazy val peopleCopy = people
  System.out.println(s"Print first time: ${peopleCopy}")
  System.out.println(s"Print second time: ${peopleCopy}")
}

This time we will assign the by-name parameter to a lazy val and then use it instead. This will only evaluate the by-name parameter once and again if we end up not using it, it will not be evaluated at all.

Let's see an example:

object Example {
  import Person._
  def main(args: Array[String]): Unit = {
    System.out.println("Now printing bad.")
    printPeopleBad(getFromDatabase())
    System.out.println("Now printing good.")
    printPeopleGood(getFromDatabase())
  }
}

If we run this application, we will see the following output:

Evaluating by name parameters only once

As you can see from the program output, the first version of our method retrieves the by-name parameter value twice, while the second version does it only once. The fact that we use a lazy val inside the second method also has the possibility of not evaluating our expensive expression at all if we don't actually use it.

Alternative lazy evaluation

There is another way to implement lazy evaluations in Scala. It is through using anonymous functions and taking advantage of the fact that functions are part of unifications in Scala and we can also pass them as parameters easily. This is done as follows: a value is represented as () => value rather than just the value itself. It is somewhat pointless, though, especially because we already have two mechanisms that can do quite a lot. Using anonymous functions for a lazy evaluation is not recommended.

Passing a function to a method can also be considered as a way of lazily evaluating some data. This, however, can be useful and should not be confused with what we just said about anonymous functions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.218.69