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
92: Enums in Scala 3 In Depth
I've spent a lot of time in Scala 2 dealing with "sealed trait" hierarchies to simulate what other languages call Enums. It worked, but it was boilerplate-heavy. Scala 3 finally gave us a first-class enum keyword, and while it looks simple on the surface, there's a lot happening under the hood that we should poke at together.
Just a list of states
Let's start with something basic. Imagine we're building a package tracking system. At first, I just need a few statuses. I'll try the simplest form of an enum:
enum DeliveryStatus {
case Pending, Shipped, Delivered, Returned
}
I can use this in a match statement, and it feels great. The compiler knows exactly which cases are possible. If I forget Returned, the compiler yells at me. That's the primary win here. But wait—what if I want to print these out or iterate through them? In the old days, I'd have to manually create a List of all possible values. Let's see if Scala 3 helps us out:
val allStatuses = DeliveryStatus.values
println(allStatuses)
// Output: Array(Pending, Shipped, Delivered, Returned)
That's handy. DeliveryStatus.values is automatically provided. It's a simple array of all the cases. This is the "pure" enumeration use case where every member is effectively a singleton.
Adding metadata to the status
Now, real-world requirements usually change five minutes after you finish the code. My boss tells me that when a package is Returned, we need to know the reason. Maybe it was "Damaged" or "Wrong Address".
If I try to just add a parameter to the Returned case like this: case Returned(reason: String), I might expect it to just work. Let's try it:
enum DeliveryStatus {
case Pending, Shipped, Delivered
case Returned(reason: String)
}
This compiles! But something interesting happened. I just turned my simple enumeration into a full-blown Algebraic Data Type (ADT). If I try to call DeliveryStatus.values now, the compiler will actually throw an error. Why? Because Returned is no longer a singleton; it's a case class. You can't have a static list of "all values" when one of those values can be any arbitrary string.
I've essentially shifted from a "Closed Set of Values" to a "Closed Set of Types". This is a critical distinction in Scala 3.
Mixing constants and data
Since we now have a mix of singletons (like Pending) and data-carrying cases (like Returned), my pattern matching has to evolve. I can't just treat them as simple constants anymore.
def getStatusMessage(status: DeliveryStatus): String = status match {
case DeliveryStatus.Pending => "Your package is waiting."
case DeliveryStatus.Shipped => "It's on the way!"
case DeliveryStatus.Delivered => "Package arrived."
case DeliveryStatus.Returned(reason) => s"Returned because: $reason"
}
I noticed that for the simple cases, I'm still using DeliveryStatus.Pending. But wait, if I'm inside a context where the compiler knows I'm dealing with DeliveryStatus, I can actually just use Pending if I import them. But more importantly, I can add custom methods directly inside the enum. This is where the "In Depth" part comes in—the enum isn't just a data structure; it's a class.
enum DeliveryStatus {
case Pending, Shipped, Delivered, Returned(reason: String)
def isFinalState: Boolean = this match {
case Delivered | Returned(_) => true
case _ => false
}
}
I love this because the logic stays with the data. I don't have to write a separate "StatusUtils" class that feels like it belongs in 2005.
The hidden power of parameters
One last thing I wanted to experiment with: what if all the statuses need a shared property, like a priority level, but they don't all take unique data? I can actually define a constructor for the enum itself.
enum DeliveryStatus(val priority: Int) {
case Pending extends DeliveryStatus(1)
case Shipped extends DeliveryStatus(2)
case Delivered extends DeliveryStatus(3)
case Returned extends DeliveryStatus(1)
}
By adding (val priority: Int) to the enum definition, I've forced every case to provide that value. Now I can do status.priority without any pattern matching at all. It's a clean way to attach metadata to every possible state without duplicating the field in every single case class.
📋 Practical Task
Implement a Payment Method Hierarchy
You are building a checkout system. Create an enum called PaymentMethod that handles three different scenarios:
- Cash: A simple state with no extra data.
- CreditCard: Needs to store a
cardNumber(String) and anexpiryDate(String). - PayPal: Needs to store an
email(String).
Additionally, implement the following requirements:
- Add a method inside the enum called
processPayment(amount: Double).- For
Cash, return:"Paid $amount in cash". - For
CreditCard, return:"Charged $amount to card ending in [last 4 digits of card]". - For
PayPal, return:"Requested $amount from [email]".
- For
- In your main code, create a
Listcontaining one of each payment method and use.foreachto print the result ofprocessPayment(100.0)for each.
There are no comments for now.