Scala
Completed
-
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
26: Building Custom Collections
Do I really have to implement every single method in the collection traits?
Short answer: No. If you tried to implement every method in Iterable or Seq manually, you'd be here all week. The magic of Scala's collection library is that it's designed around a very small "primitive" set of methods.
For most custom collections, the only thing you actually need to do is implement the iterator method. Once you provide a way to traverse your data, you automatically inherit map, filter, fold, flatMap, and dozens of other combinators for free. I always tell people to think of the iterator as the "engine" of your collection; as long as the engine works, the rest of the car just runs.
case class CircularBuffer[A](capacity: Int, private var buffer: Vector[A] = Vector.empty)
extends Iterable[A] {
def add(elem: A): Unit = {
if (buffer.length < capacity) {
buffer = buffer :+ elem
} else {
buffer = buffer.tail :+ elem
}
}
// This is the only method we MUST implement to get the Iterable powers
override def iterator: Iterator[A] = buffer.iterator
}
In the example above, because CircularBuffer extends Iterable, I can now call buffer.map(_.toString) or buffer.filter(x => x > 10) without writing a single line of logic for those operations.
Why does my custom collection return a generic Iterable instead of my own type?
This is the part that usually frustrates developers the first time they build a collection. You call myBuffer.map(...) and instead of getting back a CircularBuffer, you get a generic Iterable. This happens because the default implementation of map doesn't know how to "re-wrap" the results into your specific custom class.
In modern Scala (2.13+ and Scala 3), the library has moved away from the complex CanBuildFrom pattern, but you still have to be explicit if you want to preserve your type. The simplest way to handle this is to override the transformation methods or provide a factory. But honestly? In most real-world production code, I usually just implement a helper method or wrap the result back into the custom collection manually.
case class CircularBuffer[A](capacity: Int, private var buffer: Vector[A] = Vector.empty)
extends Iterable[A] {
override def iterator: Iterator[A] = buffer.iterator
// We override map to ensure we return a CircularBuffer instead of a generic Iterable
override def map[B](f: A => B): CircularBuffer[B] = {
val newBuffer = CircularBuffer[B](capacity)
this.foreach(item => newBuffer.add(f(item)))
newBuffer
}
def add(elem: A): Unit = {
if (buffer.length < capacity) buffer = buffer :+ elem
else buffer = buffer.tail :+ elem
}
}
It feels like a bit of boilerplate, but it's the only way to maintain the "fluent" API where you can chain operations and keep your specialized logic (like the fixed capacity) intact.
How do I make my collection truly immutable?
If you're building a collection for a multi-threaded environment, you can't just mark your internal variables as private and call it immutable. You need to change the way you update the data. Instead of modifying the internal state (like I did with var buffer above), every "mutation" method must return a new instance of your collection.
I recommend using a case class for this. Since case classes are immutable by default, you can use the copy method or simply instantiate a new version of your collection with the updated data. This is how Scala's List and Vector work under the hood—they use structural sharing so they aren't copying the entire dataset every time you add an element.
case class ImmutableCircularBuffer[A](capacity: Int, buffer: Vector[A] = Vector.empty)
extends Iterable[A] {
override def iterator: Iterator[A] = buffer.iterator
def add(elem: A): ImmutableCircularBuffer[A] = {
val nextBuffer = if (buffer.length < capacity) {
buffer :+ elem
} else {
buffer.tail :+ elem
}
// Return a brand new instance
this.copy(buffer = nextBuffer)
}
}
Now, when you call add, the original buffer remains unchanged. This is a huge win for debugging and concurrency, though it does come with a slight memory overhead because you're creating new objects.
📋 Practical Task
Exercise: Building a Time-Limited Event Log
Your task is to build a custom collection called EventLog. This collection should behave like a Seq but with a specific constraint: it should only keep the last N events added to it. If a new event is added and the log is full, the oldest event should be dropped.
- Extend
Iterable[T]. - Implement a method
log(event: T): Unitthat adds an item to the collection while maintaining the size limit. - Override the
mapmethod so that callinglog.map(...)returns a newEventLogof the same capacity, rather than a genericIterable. - Ensure the
iteratoris implemented correctly so that standard Scala methods likesumorfilterwork out of the box.
Test your implementation: Create an EventLog with a capacity of 3, add 5 different strings to it, and verify that only the last 3 are present when you print the collection.
There are no comments for now.