Lazy Evaluation and Streams: Efficient Data Processing
Introduction
Lazy evaluation is a powerful technique where computations are deferred until their results are actually needed. Scala provides several mechanisms for lazy evaluation, including lazy values, views, and streams (LazyList). This approach can significantly improve performance and memory usage, especially when working with large datasets or infinite sequences.
This lesson will teach you to leverage lazy evaluation effectively, understand when to use different lazy constructs, and build efficient data processing pipelines.
Understanding Lazy Evaluation
Lazy Values
// Eager evaluation (default)
val eagerValue = {
println("Computing eager value...")
42 * 2
}
println("Eager value created")
println(s"Using eager value: $eagerValue")
// Lazy evaluation
lazy val lazyValue = {
println("Computing lazy value...")
42 * 2
}
println("Lazy value declared")
println(s"Using lazy value: $lazyValue")
println(s"Using lazy value again: $lazyValue")
// Lazy evaluation with expensive computation
def expensiveComputation(x: Int): Int = {
println(s"Performing expensive computation for $x")
Thread.sleep(100) // Simulate expensive operation
x * x
}
lazy val expensiveResult = expensiveComputation(10)
println("Expensive computation declared")
// Only computed when first accessed
println(s"Result: $expensiveResult")
println(s"Result again: $expensiveResult") // Not computed again
// Lazy initialization pattern
class DatabaseConnection {
private lazy val connection = {
println("Establishing database connection...")
// Simulate connection setup
"connection-handle-123"
}
def query(sql: String): String = {
s"Executing '$sql' on $connection"
}
}
val db = new DatabaseConnection()
println("Database instance created")
println(db.query("SELECT * FROM users")) // Connection established here
// Lazy fields in case classes (using lazy val)
case class Person(name: String, age: Int) {
lazy val isAdult: Boolean = {
println(s"Checking if $name is adult...")
age >= 18
}
lazy val category: String = {
println(s"Determining category for $name...")
if (age < 13) "child"
else if (age < 18) "teenager"
else if (age < 65) "adult"
else "senior"
}
}
val person = Person("Alice", 25)
println(s"Person created: ${person.name}")
println(s"Is adult: ${person.isAdult}") // Computed here
println(s"Category: ${person.category}") // Computed here
println(s"Is adult again: ${person.isAdult}") // Not computed again
// Conditional lazy evaluation
def createExpensiveResource(condition: Boolean): Option[String] = {
if (condition) {
lazy val resource = {
println("Creating expensive resource...")
"expensive-resource"
}
Some(resource)
} else {
None
}
}
println("Testing conditional lazy evaluation:")
val resource1 = createExpensiveResource(false) // No computation
val resource2 = createExpensiveResource(true) // Computation deferred
println(s"Resource2: $resource2") // Computation happens here
Lazy Function Parameters
// By-name parameters (lazy evaluation)
def logAndReturn[T](message: String)(value: => T): T = {
println(s"Log: $message")
value // Evaluated here
}
def expensiveFunction(): String = {
println("Executing expensive function...")
Thread.sleep(50)
"result"
}
// By-value parameter (eager evaluation)
def eagerLog[T](message: String, value: T): T = {
println(s"Eager log: $message")
value
}
println("Testing by-name parameters:")
logAndReturn("About to compute")(expensiveFunction())
println("\nTesting by-value parameters:")
eagerLog("About to compute", expensiveFunction())
// Conditional execution with by-name parameters
def ifThenElse[T](condition: Boolean)(thenBranch: => T)(elseBranch: => T): T = {
if (condition) thenBranch else elseBranch
}
def sideEffect1(): String = {
println("Side effect 1")
"result1"
}
def sideEffect2(): String = {
println("Side effect 2")
"result2"
}
println("\nConditional execution:")
val result1 = ifThenElse(true)(sideEffect1())(sideEffect2()) // Only executes sideEffect1
val result2 = ifThenElse(false)(sideEffect1())(sideEffect2()) // Only executes sideEffect2
// Implementing lazy OR and AND operators
def lazyOr(a: => Boolean, b: => Boolean): Boolean = {
if (a) {
println("Short-circuiting OR: first condition is true")
true
} else {
println("Evaluating second condition in OR")
b
}
}
def lazyAnd(a: => Boolean, b: => Boolean): Boolean = {
if (!a) {
println("Short-circuiting AND: first condition is false")
false
} else {
println("Evaluating second condition in AND")
b
}
}
def expensiveCheck1(): Boolean = {
println("Expensive check 1")
true
}
def expensiveCheck2(): Boolean = {
println("Expensive check 2")
false
}
println("\nLazy operators:")
println(s"Lazy OR: ${lazyOr(expensiveCheck1(), expensiveCheck2())}")
println(s"Lazy AND: ${lazyAnd(expensiveCheck2(), expensiveCheck1())}")
// Memoization with lazy evaluation
class Fibonacci {
private val cache = scala.collection.mutable.Map[Int, BigInt]()
def apply(n: Int): BigInt = {
cache.getOrElseUpdate(n, {
println(s"Computing fibonacci($n)")
if (n <= 1) n
else apply(n - 1) + apply(n - 2)
})
}
}
val fib = new Fibonacci()
println("\nFibonacci with memoization:")
println(s"fib(10) = ${fib(10)}")
println(s"fib(15) = ${fib(15)}") // Uses cached values from fib(10)
// Lazy factory pattern
trait LazyFactory[T] {
lazy val instance: T = createInstance()
protected def createInstance(): T
}
class DatabaseService extends LazyFactory[String] {
protected def createInstance(): String = {
println("Initializing database service...")
"database-service-instance"
}
def getData(): String = s"Data from $instance"
}
val service = new DatabaseService()
println("Service created")
println(service.getData()) // Instance created here
Views: Lazy Collections
Creating and Using Views
// View creates a lazy version of a collection
val numbers = (1 to 1000000).toList
println(s"List created with ${numbers.length} elements")
// Eager evaluation
def timeOperation[T](name: String)(operation: => T): T = {
val start = System.nanoTime()
val result = operation
val end = System.nanoTime()
println(f"$name: ${(end - start) / 1e6}%.2f ms")
result
}
timeOperation("Eager operations") {
val result = numbers
.filter(_ % 2 == 0)
.map(_ * 2)
.filter(_ > 1000)
.take(100)
result.length
}
// Lazy evaluation with view
timeOperation("Lazy operations with view") {
val result = numbers.view
.filter(_ % 2 == 0)
.map(_ * 2)
.filter(_ > 1000)
.take(100)
.toList
result.length
}
// View transformations are lazy
val numberView = numbers.view
.filter { x =>
println(s"Filtering $x")
x % 2 == 0
}
.map { x =>
println(s"Mapping $x")
x * 2
}
println("View created - no operations executed yet")
// Operations execute when materialized
val firstFive = numberView.take(5).toList
println(s"First five: $firstFive")
// Working with infinite views
val infiniteNumbers = LazyList.from(1)
val evenSquares = infiniteNumbers.view
.filter(_ % 2 == 0)
.map(x => x * x)
println(s"First 10 even squares: ${evenSquares.take(10).toList}")
// Practical view examples
case class LogEntry(timestamp: Long, level: String, message: String)
val logEntries = (1 to 100000).map(i =>
LogEntry(
System.currentTimeMillis() - i * 1000,
if (i % 10 == 0) "ERROR" else if (i % 5 == 0) "WARN" else "INFO",
s"Log message $i"
)
).toList
// Efficient log processing with views
def processLogs(entries: List[LogEntry]): List[String] = {
entries.view
.filter(_.level == "ERROR")
.map(entry => s"${entry.timestamp}: ${entry.message}")
.take(10)
.toList
}
timeOperation("Log processing with view") {
processLogs(logEntries).length
}
// Without view (less efficient)
def processLogsEager(entries: List[LogEntry]): List[String] = {
entries
.filter(_.level == "ERROR")
.map(entry => s"${entry.timestamp}: ${entry.message}")
.take(10)
}
timeOperation("Log processing without view") {
processLogsEager(logEntries).length
}
// Chaining multiple view operations
val complexView = (1 to 1000).view
.filter(_ % 3 == 0)
.map(_ * 2)
.zipWithIndex
.filter { case (value, index) => index % 2 == 0 }
.map { case (value, index) => s"$index: $value" }
println("Complex view operations:")
complexView.take(5).foreach(println)
// Views with groupBy (materializes intermediate results)
val groupedView = numbers.view
.filter(_ < 100)
.groupBy(_ % 10)
.view
.mapValues(_.sum)
println(s"Grouped sums: ${groupedView.toMap}")
// Sliding window with views
val slidingView = (1 to 20).view
.sliding(3)
.map(_.sum)
println("Sliding window sums:")
slidingView.take(5).foreach(println)
LazyList (Streams)
Creating and Working with Streams
// LazyList (formerly Stream in Scala 2.12)
val lazyNumbers = LazyList.from(1)
println(s"Lazy list created: ${lazyNumbers}")
// Taking elements forces evaluation
val firstTen = lazyNumbers.take(10).toList
println(s"First 10 numbers: $firstTen")
// Infinite sequences
val fibonacciStream: LazyList[BigInt] = {
def fib(a: BigInt, b: BigInt): LazyList[BigInt] =
a #:: fib(b, a + b)
fib(0, 1)
}
println(s"First 20 Fibonacci numbers:")
fibonacciStream.take(20).foreach(println)
// Prime number sieve
def sieve(numbers: LazyList[Int]): LazyList[Int] = {
numbers match {
case head #:: tail =>
head #:: sieve(tail.filter(_ % head != 0))
case _ => LazyList.empty
}
}
val primes = sieve(LazyList.from(2))
println(s"First 20 primes: ${primes.take(20).toList}")
// Generating sequences with LazyList.iterate
val powersOfTwo = LazyList.iterate(1)(_ * 2)
println(s"First 10 powers of 2: ${powersOfTwo.take(10).toList}")
val collatzSequence = LazyList.iterate(13)(n => if (n % 2 == 0) n / 2 else 3 * n + 1)
val collatzSteps = collatzSequence.takeWhile(_ != 1).toList
println(s"Collatz sequence starting from 13: $collatzSteps")
// Custom stream generators
def randomStream(seed: Long): LazyList[Int] = {
val random = new scala.util.Random(seed)
LazyList.continually(random.nextInt(100))
}
val randomNumbers = randomStream(42)
println(s"First 10 random numbers: ${randomNumbers.take(10).toList}")
// Date sequence stream
import java.time.LocalDate
def dateStream(start: LocalDate): LazyList[LocalDate] =
LazyList.iterate(start)(_.plusDays(1))
val dates = dateStream(LocalDate.of(2024, 1, 1))
val weekends = dates
.filter(date => date.getDayOfWeek.getValue >= 6)
.take(10)
println("First 10 weekends of 2024:")
weekends.foreach(println)
// Combining streams
val naturalNumbers = LazyList.from(1)
val evenNumbers = naturalNumbers.filter(_ % 2 == 0)
val oddNumbers = naturalNumbers.filter(_ % 2 == 1)
// Merge two streams alternately
def mergeStreams[T](s1: LazyList[T], s2: LazyList[T]): LazyList[T] = {
(s1, s2) match {
case (h1 #:: t1, h2 #:: t2) => h1 #:: h2 #:: mergeStreams(t1, t2)
case (LazyList(), s2) => s2
case (s1, LazyList()) => s1
}
}
val alternating = mergeStreams(evenNumbers, oddNumbers)
println(s"Alternating even/odd: ${alternating.take(20).toList}")
// Stream of streams (nested lazy evaluation)
val matrixStream = LazyList.from(1).map(row =>
LazyList.from(1).map(col => row * col)
)
println("3x3 multiplication table:")
matrixStream.take(3).foreach { row =>
println(row.take(3).toList)
}
// Working with file streams (conceptual)
def fileLineStream(filename: String): LazyList[String] = {
// Simulate reading lines lazily
LazyList.from(1).map(i => s"Line $i from $filename")
}
val logLines = fileLineStream("app.log")
val errorLines = logLines
.filter(_.contains("ERROR"))
.take(5)
println("Simulated error lines:")
errorLines.foreach(println)
// Memoized streams
case class MemoizedStream[T](private val underlying: LazyList[T]) {
private val cache = scala.collection.mutable.ArrayBuffer[T]()
def apply(index: Int): T = {
if (index >= cache.length) {
val newElements = underlying.drop(cache.length).take(index - cache.length + 1)
cache ++= newElements
}
cache(index)
}
def take(n: Int): List[T] = (0 until n).map(apply).toList
}
val memoizedFib = MemoizedStream(fibonacciStream)
println(s"Memoized fib(30): ${memoizedFib(30)}")
println(s"Memoized fib(25): ${memoizedFib(25)}") // Uses cached values
// Stream comprehensions
val pythagoreanTriples = for {
c <- LazyList.from(1)
a <- LazyList.range(1, c)
b <- LazyList.range(a, c)
if a * a + b * b == c * c
} yield (a, b, c)
println("First 10 Pythagorean triples:")
pythagoreanTriples.take(10).foreach(println)
Practical Applications
Large Data Processing
// Processing large datasets efficiently
case class SalesRecord(id: Int, product: String, amount: Double, region: String, date: String)
// Simulate large dataset
val salesData = (1 to 1000000).map(i =>
SalesRecord(
i,
s"Product${i % 100}",
math.random() * 1000,
List("North", "South", "East", "West")(i % 4),
s"2024-${(i % 12) + 1:02d}-${(i % 28) + 1:02d}"
)
).toList
// Efficient aggregation with views
def salesAnalysis(data: List[SalesRecord]): Map[String, Double] = {
timeOperation("Sales analysis with view") {
data.view
.filter(_.amount > 500)
.groupBy(_.region)
.view
.mapValues(_.map(_.amount).sum)
.toMap
}
}
val regionSales = salesAnalysis(salesData)
regionSales.foreach { case (region, total) =>
println(f"$region: $$${total}%.2f")
}
// Streaming CSV processing simulation
def processCsvStream(lines: LazyList[String]): LazyList[SalesRecord] = {
lines
.filter(_.nonEmpty)
.filter(!_.startsWith("#")) // Skip comments
.map(_.split(","))
.filter(_.length >= 5)
.map { fields =>
try {
Some(SalesRecord(
fields(0).toInt,
fields(1),
fields(2).toDouble,
fields(3),
fields(4)
))
} catch {
case _: Exception => None
}
}
.collect { case Some(record) => record }
}
// Simulate CSV data stream
val csvLines = LazyList.from(1).map(i =>
s"$i,Product${i % 50},${math.random() * 1000},${List("North", "South", "East", "West")(i % 4)},2024-01-01"
)
val processedRecords = processCsvStream(csvLines)
println("Sample processed records:")
processedRecords.take(5).foreach(println)
// Pipeline processing with lazy evaluation
class DataPipeline[T] {
private var transformations: LazyList[T] => LazyList[T] = identity
def filter(predicate: T => Boolean): DataPipeline[T] = {
val prevTransform = transformations
transformations = (stream: LazyList[T]) => prevTransform(stream).filter(predicate)
this
}
def map[U](mapper: T => U): DataPipeline[U] = {
new DataPipeline[U] {
transformations = (stream: LazyList[U]) => stream
}
}
def take(n: Int): DataPipeline[T] = {
val prevTransform = transformations
transformations = (stream: LazyList[T]) => prevTransform(stream).take(n)
this
}
def process(data: LazyList[T]): LazyList[T] = transformations(data)
}
val pipeline = new DataPipeline[Int]()
.filter(_ % 2 == 0)
.filter(_ > 100)
.take(10)
val pipelineResult = pipeline.process(LazyList.from(1))
println(s"Pipeline result: ${pipelineResult.toList}")
// Real-time data processing simulation
def generateSensorData(): LazyList[Double] = {
val random = new scala.util.Random()
LazyList.continually {
Thread.sleep(10) // Simulate sensor delay
20.0 + random.nextGaussian() * 5.0 // Temperature readings
}
}
def processTemperatureData(data: LazyList[Double]): LazyList[String] = {
data
.zipWithIndex
.map { case (temp, index) =>
val status = if (temp > 30) "HIGH" else if (temp < 10) "LOW" else "NORMAL"
f"Reading $index: $temp%.1f°C ($status)"
}
.filter(_.contains("HIGH")) // Only alert on high temperatures
}
// Simulate processing first few readings
val sensorStream = generateSensorData()
val alerts = processTemperatureData(sensorStream)
println("Temperature alerts (first 5):")
alerts.take(5).foreach(println)
// Windowed operations on streams
def slidingAverage(data: LazyList[Double], windowSize: Int): LazyList[Double] = {
data
.sliding(windowSize)
.map(window => window.sum / window.length)
}
val temperatureReadings = LazyList.from(1).map(i => 20.0 + math.sin(i * 0.1) * 10)
val smoothedTemperatures = slidingAverage(temperatureReadings, 5)
println("Original vs smoothed temperatures:")
temperatureReadings.zip(smoothedTemperatures).take(10).foreach {
case (orig, smooth) => println(f"Original: $orig%.2f, Smoothed: $smooth%.2f")
}
Memory-Efficient Algorithms
// Efficient prime finding with lazy evaluation
def isPrime(n: Int): Boolean = {
if (n < 2) false
else if (n == 2) true
else !(2 to math.sqrt(n).toInt).exists(n % _ == 0)
}
def primesUpTo(limit: Int): LazyList[Int] = {
LazyList.from(2).takeWhile(_ <= limit).filter(isPrime)
}
timeOperation("Find primes up to 10000") {
val primeCount = primesUpTo(10000).length
println(s"Number of primes: $primeCount")
}
// Memory-efficient file processing
def processLargeFile(lines: LazyList[String]): (Int, Int, Map[String, Int]) = {
val stats = lines
.view
.zipWithIndex
.map { case (line, index) =>
val words = line.split("\\s+").filter(_.nonEmpty)
(1, words.length, words.groupBy(identity).view.mapValues(_.length).toMap)
}
.foldLeft((0, 0, Map.empty[String, Int])) {
case ((lineCount, wordCount, wordFreq), (lines, words, freq)) =>
val newWordFreq = (wordFreq.keySet ++ freq.keySet).map { word =>
word -> (wordFreq.getOrElse(word, 0) + freq.getOrElse(word, 0))
}.toMap
(lineCount + lines, wordCount + words, newWordFreq)
}
stats
}
// Simulate file content
val fileContent = LazyList.from(1).map(i => s"line $i with some repeated words and different content")
val (totalLines, totalWords, wordFrequency) = processLargeFile(fileContent.take(1000))
println(s"Lines: $totalLines, Words: $totalWords")
println("Top 5 words:")
wordFrequency.toSeq.sortBy(-_._2).take(5).foreach {
case (word, count) => println(s" $word: $count")
}
// Graph traversal with lazy evaluation
case class Node(id: Int, connections: List[Int])
class LazyGraph(nodes: Map[Int, Node]) {
def breadthFirstSearch(start: Int): LazyList[Int] = {
def bfs(queue: LazyList[Int], visited: Set[Int]): LazyList[Int] = {
queue match {
case current #:: rest if !visited.contains(current) =>
val neighbors = nodes.get(current).map(_.connections).getOrElse(List.empty)
val newQueue = rest ++ neighbors.filter(!visited.contains(_))
current #:: bfs(newQueue, visited + current)
case current #:: rest =>
bfs(rest, visited)
case _ =>
LazyList.empty
}
}
bfs(LazyList(start), Set.empty)
}
def depthFirstSearch(start: Int): LazyList[Int] = {
def dfs(stack: List[Int], visited: Set[Int]): LazyList[Int] = {
stack match {
case current :: rest if !visited.contains(current) =>
val neighbors = nodes.get(current).map(_.connections).getOrElse(List.empty)
current #:: dfs(neighbors ++ rest, visited + current)
case current :: rest =>
dfs(rest, visited)
case Nil =>
LazyList.empty
}
}
dfs(List(start), Set.empty)
}
}
// Create a sample graph
val graphNodes = Map(
1 -> Node(1, List(2, 3)),
2 -> Node(2, List(1, 4, 5)),
3 -> Node(3, List(1, 6)),
4 -> Node(4, List(2)),
5 -> Node(5, List(2, 6)),
6 -> Node(6, List(3, 5))
)
val graph = new LazyGraph(graphNodes)
println("BFS traversal:")
graph.breadthFirstSearch(1).take(10).foreach(println)
println("DFS traversal:")
graph.depthFirstSearch(1).take(10).foreach(println)
// Lazy tree operations
sealed trait Tree[+T]
case object Empty extends Tree[Nothing]
case class Branch[T](value: T, left: Tree[T], right: Tree[T]) extends Tree[T]
object Tree {
def fromList[T](list: List[T]): Tree[T] = {
def build(items: List[T]): Tree[T] = items match {
case Nil => Empty
case head :: tail =>
val (left, right) = tail.splitAt(tail.length / 2)
Branch(head, build(left), build(right))
}
build(list)
}
def traverse[T](tree: Tree[T]): LazyList[T] = tree match {
case Empty => LazyList.empty
case Branch(value, left, right) =>
traverse(left) ++ LazyList(value) ++ traverse(right)
}
}
val tree = Tree.fromList((1 to 15).toList)
val treeTraversal = Tree.traverse(tree)
println(s"Tree traversal: ${treeTraversal.take(10).toList}")
Best Practices and Performance Tips
When to Use Lazy Evaluation
// Guidelines for using lazy evaluation effectively
// 1. Use lazy vals for expensive computations that might not be needed
class ConfigurationManager {
lazy val databaseConfig = {
println("Loading database configuration...")
// Expensive file reading or network call
Map("host" -> "localhost", "port" -> 5432)
}
lazy val cacheConfig = {
println("Loading cache configuration...")
Map("ttl" -> 3600, "maxSize" -> 1000)
}
def getDatabaseHost: String = databaseConfig("host").toString
// This might never be called, so cache config won't be loaded
def getCacheTtl: Int = cacheConfig("ttl").toString.toInt
}
val config = new ConfigurationManager()
println(s"Database host: ${config.getDatabaseHost}")
// Cache config is never loaded
// 2. Use views for chain operations on large collections
def efficientDataProcessing(data: List[Int]): List[Int] = {
if (data.length > 10000) {
// Use view for large datasets
data.view
.filter(_ % 2 == 0)
.map(_ * 3)
.filter(_ > 100)
.take(50)
.toList
} else {
// Regular operations for small datasets
data
.filter(_ % 2 == 0)
.map(_ * 3)
.filter(_ > 100)
.take(50)
}
}
// 3. Use LazyList for potentially infinite sequences
def generateReports(): LazyList[String] = {
LazyList.from(1).map { reportId =>
// Each report is generated only when requested
s"Report $reportId: ${generateReportContent(reportId)}"
}
}
def generateReportContent(id: Int): String = {
s"Content for report $id (generated at ${System.currentTimeMillis()})"
}
val reports = generateReports()
println("First 3 reports:")
reports.take(3).foreach(println)
// 4. Avoid lazy evaluation pitfalls
class LazyPitfalls {
// Pitfall 1: Lazy vals with side effects
var counter = 0
lazy val problematicLazy = {
counter += 1 // Side effect - unpredictable when it happens
counter
}
// Better: Use def for operations with side effects
def incrementCounter(): Int = {
counter += 1
counter
}
// Pitfall 2: Memory leaks with large lazy structures
lazy val largeData = (1 to 1000000).toList // Might consume a lot of memory
// Better: Use streams or views
def largeDataStream = LazyList.from(1).take(1000000)
// Pitfall 3: Exception handling with lazy values
lazy val riskyOperation = {
val result = 10 / 0 // This will throw an exception
result
}
// Better: Handle exceptions explicitly
lazy val safeOperation = {
try {
Some(10 / 0)
} catch {
case _: ArithmeticException => None
}
}
}
// 5. Measuring lazy evaluation performance
def measureLazyPerformance(): Unit = {
val data = (1 to 100000).toList
// Eager evaluation
timeOperation("Eager evaluation") {
val result = data
.filter(_ % 2 == 0)
.map(_ * 2)
.filter(_ > 1000)
.take(100)
result.length
}
// Lazy evaluation with view
timeOperation("Lazy evaluation") {
val result = data.view
.filter(_ % 2 == 0)
.map(_ * 2)
.filter(_ > 1000)
.take(100)
.toList
result.length
}
// LazyList
timeOperation("LazyList") {
val result = LazyList.from(1)
.filter(_ % 2 == 0)
.map(_ * 2)
.filter(_ > 1000)
.take(100)
.toList
result.length
}
}
measureLazyPerformance()
// 6. Best practices summary
object LazyEvaluationBestPractices {
// Use lazy val for expensive initialization
lazy val expensiveResource = initializeExpensiveResource()
// Use views for intermediate collection operations
def processCollection[T](data: List[T], transform: T => T): List[T] = {
data.view.map(transform).filter(_ != null).toList
}
// Use LazyList for infinite or very large sequences
def infiniteSequence[T](generator: Int => T): LazyList[T] = {
LazyList.from(0).map(generator)
}
// Use by-name parameters for conditional evaluation
def debug(condition: Boolean)(message: => String): Unit = {
if (condition) println(s"DEBUG: $message")
}
private def initializeExpensiveResource(): String = {
Thread.sleep(100)
"expensive-resource"
}
}
// Usage examples
LazyEvaluationBestPractices.debug(true)("This will be evaluated")
LazyEvaluationBestPractices.debug(false)("This will NOT be evaluated")
val processed = LazyEvaluationBestPractices.processCollection(
List("a", "b", "c"),
_.toUpperCase
)
println(s"Processed: $processed")
val sequence = LazyEvaluationBestPractices.infiniteSequence(i => i * i)
println(s"First 5 squares: ${sequence.take(5).toList}")
Summary
In this lesson, you've mastered lazy evaluation and streams in Scala:
â
Lazy Values: Deferring expensive computations until needed
â
By-Name Parameters: Lazy function argument evaluation
â
Views: Lazy collection operations for performance
â
LazyList: Working with infinite and large sequences
â
Practical Applications: Real-world lazy evaluation patterns
â
Performance: Optimizing memory and computation
â
Best Practices: When and how to use lazy evaluation effectively
Lazy evaluation is a powerful technique for building efficient, memory-conscious applications that can handle large datasets and infinite sequences gracefully.
What's Next
In the next lesson, we'll explore concurrency and parallelism in Scala, learning how to write concurrent programs using Futures, parallel collections, and actor systems.
Comments
Be the first to comment on this lesson!