-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
138: Practice Exercise: Implementing a Persistent Queue
When you first start thinking about queues in a functional language like Scala, there's a very tempting path you'll likely want to take. You'll think: "I'll just use a List and append new elements to the end using the :+ operator." It seems intuitive. It looks clean. But if you do this, you've accidentally built a performance time bomb.
The "Just Append to a List" Trap
Let's look at why that "simple" approach fails. In Scala, a List is a singly-linked list. Adding an element to the front (prepending) is a constant time $O(1)$ operation. But appending to the end requires the runtime to traverse the entire list to find the last element and then rebuild the list structure to maintain immutability.
// This looks fine, but it's O(n)
def enqueue(q: List[Int], item: Int): List[Int] = q :+ item
If you have a queue with 10,000 elements, every single enqueue operation has to walk past those 10,000 elements. That's not a queue; that's a bottleneck. In a real production system, this would lead to an exponential slowdown as your data grows. I've seen this mistake land in code reviews more times than I can count because the syntax :+ hides the cost so effectively.
The Two-List Dance for Amortized Efficiency
To get the performance we actually need—amortized $O(1)$—we have to change how we store the data. Instead of one list, we use two. I like to think of them as the "In-box" and the "Out-box".
- The In-box: Where we push new elements. We always prepend here, so
enqueueis always $O(1)$. - The Out-box: Where we pull elements from. We take from the head, so
dequeueis usually $O(1)$.
The "magic" happens when the Out-box becomes empty. Instead of failing, we take the entire In-box, reverse it, and make it the new Out-box. While reversing a list is an $O(n)$ operation, it only happens occasionally. If you spread that cost over all the elements that were pushed into the In-box, the average cost per operation remains constant. This is what we call "amortized" time complexity.
case class PersistentQueue[A](in: List[A] = Nil, out: List[A] = Nil) {
def enqueue(elem: A): PersistentQueue[A] =
copy(in = elem :: in, out = out)
def dequeue(): (A, PersistentQueue[A]) = out match {
case head :: tail =>
(head, copy(in = in, out = tail))
case Nil =>
// The "pivot" moment: reverse 'in' and move it to 'out'
val reversed = in.reverse
reversed match {
case head :: tail => (head, copy(in = Nil, out = tail))
case Nil => throw new NoSuchElementException("Queue is empty")
}
}
}
Notice how we never mutate anything. Each operation returns a new PersistentQueue. Because the underlying lists share tails, we aren't actually copying the whole data structure every time—we're just creating a few new nodes that point to existing ones. This is the core beauty of persistent data structures.
📋 Practical Task
Implementing a Purely Functional Persistent Queue
Your task is to implement a persistent queue from scratch. Instead of using a case class with default arguments, implement it using a sealed trait to handle the empty state more explicitly (avoiding the need to throw a NoSuchElementException).
Requirements:
- Create a sealed trait
MyQueue[+A]. - Implement an
Emptyobject that extendsMyQueue. - Implement a
Queuecase class that tracks theinandoutlists. - Implement an
enqueue(elem: A): MyQueue[A]method. - Implement a
dequeue(): Option[(A, MyQueue[A])]method. It should returnNoneif the queue is empty, andSome((value, nextQueue))otherwise. - Ensure that the
dequeuelogic correctly handles the transfer and reversal from theinlist to theoutlist when theoutlist is exhausted.
Test your implementation with the following sequence:
val q0 = MyQueue.Empty
val q1 = q0.enqueue(1).enqueue(2).enqueue(3)
val (val1, q2) = q1.dequeue().get // Should be 1
val (val2, q3) = q2.dequeue().get // Should be 2
val q4 = q3.enqueue(4)
val (val3, q5) = q4.dequeue().get // Should be 3
val (val4, q6) = q5.dequeue().get // Should be 4
There are no comments for now.