-
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
47: Practice Exercise: Building a Simple Expression Evaluator with Pattern Matching
Think of building an expression evaluator like working at a postal sorting center. When a package arrives, you don't just guess what's inside; you look at the label. If it's labeled "Fragile," you handle it with gloves. If it's labeled "Overnight," it goes to the express lane. If it's a "Large Crate," you call in the forklift. The label tells you exactly which set of rules to apply, and once you've handled that package, you might find another smaller package tucked inside that needs its own sorting process.
In Scala, we do the exact same thing with Algebraic Data Types (ADTs) and pattern matching. The "labels" are our case classes, and the "sorting rules" are our match statements. Because mathematical expressions are naturally recursive—an addition operation contains two other expressions, which could themselves be additions or multiplications—pattern matching is the only sane way to handle this without drowning in a sea of if/else blocks.
Mapping Math to Case Classes
To get this working, we first need to define what an "expression" actually is. I prefer using a sealed trait here. By marking it sealed, I'm telling the compiler, "Look, these are the only types of expressions that will ever exist." This is a lifesaver because if I add a new operation later but forget to update my evaluator, the compiler will yell at me for having an incomplete match.
sealed trait Expr
case class Val(value: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Mul(left: Expr, right: Expr) extends Expr
Notice how Add and Mul don't just take integers; they take other Expr objects. This is the "nesting doll" part of the analogy. A Mul expression could contain two Vals, or it could contain an Add expression and a Val.
The Recursive Evaluation Loop
Now we need the logic to actually crunch the numbers. I like to write this as a single function that calls itself. When the evaluator hits a Val, it's reached the "bottom" of the tree and can just return the number. If it hits an Add or Mul, it pauses, evaluates the left side, evaluates the right side, and then performs the math.
def eval(expr: Expr): Int = expr match {
case Val(n) => n
case Add(l, r) => eval(l) + eval(r)
case Mul(l, r) => eval(l) * eval(r)
}
I've seen some developers try to implement this using an object-oriented approach where each class has an evaluate() method. While that works, it scatters the logic across four different classes. By using pattern matching, the entire "business logic" of how math works lives in one place. It's much easier to maintain when you realize you need to add a Subtract or Divide operation later.
Putting it Together in Practice
If we want to represent (3 + 5) * 2, we don't write a string; we build the tree structure. It looks like this in code:
val expression = Mul(Add(Val(3), Val(5)), Val(2))
println(eval(expression)) // Output: 16
The eval function sees the Mul, dives into the Add, grabs the 3 and 5, sums them to 8, and then finally multiplies that by 2. It's an elegant dance that lets the language handle the stack management for you.
📋 Practical Task
Exercise: Extending the Evaluator for Division and Negative Values
Your goal is to take the basic evaluator we just built and make it more robust. In the real world, math isn't just addition and multiplication.
Requirements:
- Modify the
Exprtrait and its subclasses to include aDiv(Division) operation. - Add a
Neg(Negation) operation that takes a singleExprand flips its sign (e.g.,Neg(Val(5))should result in-5). - Update the
evalfunction to handle these new cases. - Bonus: Implement a check in the
Divcase to throw anArithmeticExceptionif the right-hand expression evaluates to zero, rather than letting the JVM crash with a raw error.
Test your implementation with this expression:
(10 / (2 + 3)) * -2
(Represented as: Mul(Div(Val(10), Add(Val(2), Val(3))), Neg(Val(2))))
Expected result: -4
There are no comments for now.