Last updated on
Week 7: Laziness
Congrats on completing week 7 of CS-214, and we hope you enjoyed the fall break! Here is a round-up of administrative announcements, interesting questions, and general notes on the course.
Administrivia
- Midterm exam. Next week Wednesday 4 November we have the midterm exam scheduled. Please be reminded of the midterm study guide, which contains logistical information as well as tips on how to best prepare for the exam.
- No lecture. There will be no lecture on Wednesday due to the midterm.
- Unguided callback. The check-offs for the unguided callback have started. As per the callback guidelines, it’s useful to rehearse your demo upfront, and review the grading scheme.
Interesting Ed questions and links
- Is there a bug in the code or in the specification?
- How to Git like a pro, in a team. Learn Git Branching provides useful exercises, in particular the “Remote” section.
- If you have trouble with variance, we have a detailed guide on that too!
Lazy laziness
In the Laziness exercises you were asked to implement a lazy list of natural numbers using itself and map:
lazy val naturalNumbers2: MyLazyList[Int] = cons(0, ...)
One might wonder what is the purpose of making a val lazy when you’re using the MyLazyList.cons already. Let’s try it out by debugging a few variants.
For demonstration purposes, we (wrongly) implement the value as follows:
lazy val naturalNumbersLazyLazy : MyLazyList[Int] = MyLazyList.cons(
0,
naturalNumbersLazyLazy
)
2026-10-30/lazy_vals.worksheet.sc
Does annotating it with lazy have an impact on the initialization of the value? i.e. is its execution equivalent to the “greedy” val below:
val naturalNumbersGreedyLazy : MyLazyList[Int] = MyLazyList.cons(
0,
naturalNumbersGreedyLazy
)
2026-10-30/lazy_vals.worksheet.sc
When in doubt, debug! Let’s add various print statements to discover the evaluation order.
To the lazy list cons we add a print:
def cons[A](elem: => A, tail: => MyLazyList[A]): MyLazyList[A] =
println("Prefixing wisdom...")
MyLazyList(() => LZCons(elem, tail))
2026-10-30/lazy_vals.worksheet.sc
And we define vals with prints in their cons arguments:
println("Setting up our lazy-lazy oracle...")
lazy val oracleLazyLazy : MyLazyList[Int] = MyLazyList.cons(
{println("Computing initial answer..."); 42},
{println("Then sum more..."); oracleLazyLazy}
)
println("Done!")
println(f"Answer: ${oracleLazyLazy.head}\n")
println("Setting up our greedy-lazy oracle...")
val oracleGreedyLazy : MyLazyList[Int] = MyLazyList.cons(
{println("Computing initial answer..."); 42},
{println("Then sum more..."); oracleGreedyLazy}
)
println("Done!")
println(f"Answer: ${oracleGreedyLazy.head}")
2026-10-30/lazy_vals.worksheet.sc
Can you guess its output?
Output
The output is as follows:
Setting up our lazy-lazy oracle...
Done!
Prefixing wisdom...
Computing initial answer...
Then sum more...
Answer: 42
Setting up our greedy-lazy oracle...
Prefixing wisdom...
Done!
Computing initial answer...
Then sum more...
Answer: 42
From the logs we can infer that the difference between a lazily defined lazy list and a “greedily” defined lazy list is the evaluation of the top-level function call to the list cons. This follows the intended behaviour of lazy val – that we postpone initialization until the value is needed.
What would be the purpose of lazily defining the lazy list? One motivation could be when the construction cons of the lazy list itself is expensive.
Code quality improvements
Here are a few common code smells and code style improvements, based on your lab submissions:
Watch out for precision loss when casting
When doing math with floating-point numbers (like computing pixel brightness in Grayscale or applying blur kernels), pay close attention to when you convert values to Int.
Converting intermediate numbers to Int too early rounds off the decimal part at every step. Those little truncations add up, leading to unnecessary precision loss and slightly darker or distorted colors. Keep intermediary results as floats, and only call .toInt once at the very end when creating your final pixel:
Before:
// Converting to Int too early throws away decimals and loses precision along the way
val rLuma = (red(pixel) * 0.299f).toInt
val gLuma = (green(pixel) * 0.587f).toInt
val bLuma = (blue(pixel) * 0.114f).toInt
val luma = rLuma + gLuma + bLuma
2026-10-30/smells.worksheet.sc
After:
// Keep floats all the way through, and only convert to Int once at the very end
val luma = (red(pixel) * 0.299f + green(pixel) * 0.587f + blue(pixel) * 0.114f).toInt
2026-10-30/smells.worksheet.sc
Don’t compute things you might not need
Precomputing values before an if/else can be good for readability when the computation is cheap, it names intermediate values and keeps branch bodies clean.
However, in performance-critical code running millions of times (like per-pixel operations in image processing), computing values before branching can waste significant CPU time if those values are only needed in one branch.
For instance, in conditional filters like RedSplash:
Before:
val px = src(x, y)
val gray = grayscale(px) // Precomputed even if the pixel is already red and we never need it!
if isRedEnough(px) then px else gray
2026-10-30/smells.worksheet.sc
After:
Only do the work inside the branch that actually needs it:
val px = src(x, y)
// Only compute grayscale when we actually need it
if isRedEnough(px) then px else grayscale(px)
2026-10-30/smells.worksheet.sc
If you still prefer declaring an intermediate expression for readability, lazy val gives you a middle ground by keeping the name at the top with minimal overhead and impact on performance.
val px = src(x, y)
lazy val gray = grayscale(px)
if isRedEnough(px) then px else gray
2026-10-30/smells.worksheet.sc
Combining channel data: Prefer expressive composition over manual accumulators
When computing pixel averages in SimpleBlur, managing four mutable accumulators (var aSum, rSum, gSum, bSum) in a loop requires readers to track state changes line-by-line:
Imperative accumulation:
val pixels: Seq[Pixel] = ???
var aSum, rSum, gSum, bSum = 0
for px <- pixels do
aSum += alpha(px)
rSum += red(px)
gSum += green(px)
bSum += blue(px)
val count = pixels.size
argb(aSum / count, rSum / count, gSum / count, bSum / count)
2026-10-30/smells.worksheet.sc
A common first functional attempt is to compute each channel separately with .map:
val pixels: Seq[Pixel] = ???
// Better than mutable vars,
// but duplicates logic and traverses `pixels` 4 separate times
val count = pixels.size
argb(
pixels.map(alpha).sum / count,
pixels.map(red).sum / count,
pixels.map(green).sum / count,
pixels.map(blue).sum / count
)
2026-10-30/smells.worksheet.sc
While this avoids mutable state, it repeats identical logic four times and iterates through pixels four separate times.
Idiomatic approaches:
The scalashop lab provides separateChannels, unzip4, and map4 in common.scala and ARGB.scala. They let you transform all 4 color channels together in one pass:
val pixels: Seq[Pixel] = ???
val channels = pixels.map(separateChannels).unzip4
argb(channels.map4(_.sum / pixels.size))
2026-10-30/smells.worksheet.sc
Alternatively, treating the extractor functions as values and mapping over them is another concise way to eliminate repetition:
val pixels: Seq[Pixel] = ???
val List(a, r, g, b) = List(alpha, red, green, blue)
.map(c => pixels.map(c).sum / pixels.size)
argb(a, r, g, b)
2026-10-30/smells.worksheet.sc
Choose fine-grained tasks over coarse splits for parallel work
When dividing work across threads (such as in ParImage.build), a common pitfall is creating only as many chunks as CPU cores (e.g. dividing the image into 4 or 8 big slices).
While this might work if every item takes the exact same amount of time, in practice workloads are rarely uniform. If one thread finishes early, it sits idle waiting for the slowest thread to finish (load imbalance).
Slicing work into smaller, finer-grained tasks (like row-by-row instead of a few big chunks) lets a scheduler (like ForkJoinPool) dynamically balance the load: threads that finish early can pick up remaining tasks and keep all cores busy.
Before:
// Dividing into only as many big chunks as threads leads to idle cores (load imbalance)
val chunkSize = (totalItems + parallelization - 1) / parallelization
val chunks = (0 until totalItems by chunkSize).par
chunks.foreach { from =>
processChunk(from, math.min(from + chunkSize, totalItems))
}
2026-10-30/smells.worksheet.sc
After:
// Slicing into finer-grained tasks allows idle threads to dynamically steal work
val tasks = (0 until totalItems).par
tasks.foreach { item =>
processItem(item)
}
2026-10-30/smells.worksheet.sc