Lazy Val in Scala

  1. Use lazy val in Scala
  2. Infinite Data Structure With lazy val
  3. Advantages of Using lazy val in Scala
  4. Disadvantages of Using lazy val in Scala
Lazy Val in Scala

This article will tackle how lazy val works in Scala. We will also discuss some disadvantages it has.

Use lazy val in Scala

lazy val is a feature that defers the initialization of a variable, a typical pattern used in Java programs. It is also called a call by need evaluation strategy where the statement is not evaluated until its first use, meaning to postpone or defer the evaluation until demanded.

We only have to add the keyword lazy before val to use this. Let’s see how it works with an example.

The below code statements are executed in scala REPL.

Without lazy:

scala> val marks = List(20,30,40,50)
val marks: List[Int] = List(20, 30, 40, 50)

scala> val doubleMarks = marks.map(temp=>temp*2)    //when we hit enter after statement , we get below result: the doubleMarks list is calculated
val doubleMarks: List[Int] = List(40, 60, 80, 100)


scala> println(doubleMarks)
List(40, 60, 80, 100)

With lazy:

scala> val marks = List(50,60,70,25)
val marks: List[Int] = List(50, 60, 70, 25)

scala> lazy val doubleMarks = marks.map(temp=>temp*2)
lazy val doubleMarks: List[Int] // unevaluated

scala> println(doubleMarks)
List(100, 120, 140, 50)

The Scala compiler does not immediately evaluate the lazy val expression. It is only assessed on its first access.

When the initial access is done, the compiler evaluates the expression and then stores (caches) the lazy val result. No execution happens whenever we access this later, and the compiler returns the stored result.

Let’s see an example regarding this caching/storing of lazy val result:

scala> lazy val statement={ println("first time access and storing it"); 1000 }
lazy val statement: Int // unevaluated

scala> println(statement)
first time access and storing it
1000

scala> println(statement)
1000

When we print/access the statement for the first time, we get 1000 along with a print statement, but when it is accessed a second time, we only get 1000 because it is the value assigned and stored/cached in a variable statement.

Infinite Data Structure With lazy val

lazy val gives access to the infinite data structure.

In Scala, we know that lists are sequences. And these lists are also strict sequences means that the list elements are constructed upfront, but we can also create non-strict sequences where the elements are made as per the requirement.

Example code:

object workspace {
    def main(args: Array[String]): Unit = {
        var temp = 5

        def myfun() = {
            temp += 1;
            temp
        }

        lazy val geek = Stream.continually( myfun() )

        (geek take 5) foreach {
            x => println(2*x)
        }
    }
}

Output:

12
14
16
18
20

The myfun() method is converted to a function. Stream.continually() method creates an infinite stream.

It takes the function, and whenever each element is accessed, the myfun() function is called only at that time to perform the computation.

This on-demand execution of a function is known as Thunk, and once it is computed, it is stored in cache for further use, which is also termed memoization.

Advantages of Using lazy val in Scala

lazy val optimizes the computation process. In the example, we can observe that we have wasted our map operations (CPU computations) which are very costly when dealing with more extensive and more complex code.

So here, lazy evaluations help us optimize the process by evaluating the expression only when needed, avoiding unnecessary computations.

Apace Spark, a Big Data framework, exploits this functionality a lot as it deals with a very massive amount of data. lazy val also gives access to the infinite data structure.

Disadvantages of Using lazy val in Scala

Despite its advantages, it has some disadvantages, so it has to be used with caution.

lazy val might lead to a deadlock when dealing with multiple threads.

Example code:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent._
import scala.concurrent.duration.DurationInt

object myFirstObj {
    lazy val initialState = 50
    lazy val start = mySecondObj.initialState
}

object mySecondObj {
    lazy val initialState = myFirstObj.initialState
}

object workspace extends App {
    def run = {
        val result = Future.sequence(Seq(Future {
        myFirstObj.start
        },
        Future {
            mySecondObj.initialState
        }
        ))
        Await.result(result, 20.second)
    }
    run
}

Output:

Exception in thread "main" java.util.concurrent.TimeoutException: Future timed out after [20 seconds]
    at scala.concurrent.impl.Promise$DefaultPromise.tryAwait0(Promise.scala:248)
    at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:261)
    at scala.concurrent.Await$.$anonfun$result$1(package.scala:201)
    at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:62)
    at scala.concurrent.Await$.result(package.scala:124)
    at workspace$.run(workspace.scala:23)
    at workspace$.delayedEndpoint$workspace$1(workspace.scala:25)
    at workspace$delayedInit$body.apply(workspace.scala:14)
    at scala.Function0.apply$mcV$sp(Function0.scala:39)
    at scala.Function0.apply$mcV$sp$(Function0.scala:39)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
    at scala.App.$anonfun$main$1(App.scala:76)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:563)
    at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:561)
    at scala.collection.AbstractIterable.foreach(Iterable.scala:926)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at workspace$.main(workspace.scala:14)
    at workspace.main(workspace.scala)

We can see that we get a time-out exception here. The Future initializes myFirstObj, and the instance of FirstObj internally tries to initialize SecondObj.

Also, the Future at the next step tries to initialize the mySecondObj. This leads to a potential deadlock situation.

Moreover, the space complexity of the program may increase as we might have to store/cache a lot of expressions. Debugging could become tricky here as the programmer has no control over program execution.

Author: Suraj P
Suraj P avatar Suraj P avatar

A technophile and a Big Data developer by passion. Loves developing advance C++ and Java applications in free time works as SME at Chegg where I help students with there doubts and assignments in the field of Computer Science.

LinkedIn GitHub