/ scala

TOTD: Scala - Lazy Evaluation

In Scala, it is possible to define lazy function arguments by using : => notation. When passing a normal argument is called by value. When making them into a lazy thunk, it is said to be called by name. This allows you to pass in a block that won't be evaluated until it is referenced. To demonstrate, let's create a simple if-else DSL.

def _if[A](Cond: Boolean)
    (trueBlock: => A)
    (falseBlock: => A) =
  if (Cond) trueBlock else falseBlock

// Let's use it!
_if ("1".toInt == 2)
{
    // this block never gets called...
    println("It's true!") 
}
{
    // but this block is executed
    println("It's false!")
}

Suppose we want the ability to compose deferred blocks of code in for-comprehensions. Luckily for us, Cats already has a faithful implementation that we could use.

import cats.Eval
import cats.syntax.applicative._

var counter = 0

val later = Eval.later { counter += 1; counter }

later.value // 1
later.value // 1, memoised

Here, we wrap out block in the Eval.later factory function. With this setup, we are able to delay the execution of our block until we get its value. However, if we call it again, it will only retreive its cached value. This is great for expensive computations.

Here is another example:

var counter = 0

val always = Eval.always { counter += 1; counter }

always.value // 1
always.value // 2, re-evaluated

Here, we wrap our block within another lazy factory function, but this time around, there is no caching. This is useful when there is a need for a delay and caching doesn't make sense.

The best part about being able to control the computation of code blocks is that they can now be composed.

val printA = Eval.now { "This runs immediately!" }
val printB = Eval.later { "Later runs once when value is accessed!" }
val printC = Eval.always { "Always runs every time but is lazy!" }

val lazies = for {
  a <- Eval.later { "Expensive computation" }
  
  // lift a regular value into the Eval context
  b <- 2.pure[Eval]
  
  _ <- printA
  _ <- printB
  _ <- printC
  
  // bonus singletons, wrapped in Eval.now's
  c <- Eval.True
  d <- Eval.False
  e <- Eval.One
  f <- Eval.Zero
  _ <- Eval.Unit
  
} yield (a, b, c, d, e, f)

lazies.value // run the lazies!

Run this in Scastie after importing the latest version of Cats. Remember to stay wise.

Closing notes: A lazy val gets de-sugared as a run-once def. val's are just constants on the stack, while lazy val's are run-once thunks a.k.a. functions with no params, which always returns the same value. And finally, Evals trampoline instead of utilizing the stack, which means it evaluates on the heap.