case classes are special classes that can be used in pattern matching with case expressions. A case class is concise and easy to create, and it exposes each of its constructor parameters as values. You can use case classes to create lightweight value objects or data holders with meaningful names for the class and its properties.
Suppose we want to receive and process stock-trading transactions. The messages for selling and buying might be accompanied by information such as the name of a stock and a quantity. It’s convenient to store this information in objects, but how would we pattern-match them? This is the purpose of case classes. These are classes that the pattern matcher readily recognizes and matches. Here’s an example of a few case classes:
PatternMatching/TradeStock.scala | |
| trait Trade |
| case class Sell(stockSymbol: String, quantity: Int) extends Trade |
| case class Buy(stockSymbol: String, quantity: Int) extends Trade |
| case class Hedge(stockSymbol: String, quantity: Int) extends Trade |
We’ve defined Trade as a trait since we don’t expect any direct instances of it, much like the way we’d define an interface in Java. We’ve extended the case classes Sell, Buy, and Hedge from it. These three take a stock symbol and quantity as their constructor parameters.
Now we can readily use these in case statements:
PatternMatching/TradeStock.scala | |
| object TradeProcessor { |
| def processTransaction(request : Trade) { |
| request match { |
| case Sell(stock, 1000) => println(s"Selling 1000-units of $stock") |
| case Sell(stock, quantity) => |
| println(s"Selling $quantity units of $stock") |
| case Buy(stock, quantity) if (quantity > 2000) => |
| println(s"Buying $quantity (large) units of $stock") |
| case Buy(stock, quantity) => |
| println(s"Buying $quantity units of $stock") |
| } |
| } |
| } |
We match the request against Sell and Buy. The stock symbol and quantity we receive are matched and stored in the pattern variables stock and quantity, respectively. We can specify constant values, like 1000 for quantity, or even use a guarded match, like checking if quantity > 2000. Here’s an example of using the TradeProcessor singleton:
PatternMatching/TradeStock.scala | |
| TradeProcessor.processTransaction(Sell("GOOG", 500)) |
| TradeProcessor.processTransaction(Buy("GOOG", 700)) |
| TradeProcessor.processTransaction(Sell("GOOG", 1000)) |
| TradeProcessor.processTransaction(Buy("GOOG", 3000)) |
The output from the code is shown here:
| Selling 500 units of GOOG |
| Buying 700 units of GOOG |
| Selling 1000-units of GOOG |
| Buying 3000 (large) units of GOOG |
In the example, all the concrete case classes took parameters. If you have a case class that takes no parameter, then place empty parentheses after the class name to indicate an empty parameter list—otherwise the Scala compiler will generate a warning.
There’s one other complication when dealing with case classes that take no parameters—use caution when passing them as messages. In this example, we have case classes that don’t take any parameters:
PatternMatching/ThingsAcceptor.scala | |
| case class Apple() |
| case class Orange() |
| case class Book () |
| |
| object ThingsAcceptor { |
| def acceptStuff(thing: Any) { |
| thing match { |
| case Apple() => println("Thanks for the Apple") |
| case Orange() => println("Thanks for the Orange") |
| case Book() => println("Thanks for the Book") |
| case _ => println(s"Excuse me, why did you send me $thing") |
| } |
| } |
| } |
In the following code, we forgot to place parentheses next to Apple in the last call:
PatternMatching/ThingsAcceptor.scala | |
| ThingsAcceptor.acceptStuff(Apple()) |
| ThingsAcceptor.acceptStuff(Book()) |
| ThingsAcceptor.acceptStuff(Apple) |
The result of the calls is shown here:
| Thanks for the Apple |
| Thanks for the Book |
| Excuse me, why did you send me Apple |
When we forgot the parentheses, instead of sending an instance of the case class, we are sending its companion object. The companion object mixes in the scala.Function0 trait, meaning it can be treated as a function. So, we end up sending a function instead of an instance of the case class. If the acceptStuff method received an instance of a case class named Thing, this would not be a problem. Let’s give that idea a try.
| abstract class Thing |
| case class Apple() extends Thing |
| |
| object ThingsAcceptor { |
| def acceptStuff(thing: Thing) { |
| thing match { |
| //... |
| case _ => |
| } |
| } |
| } |
| |
| ThingsAcceptor.acceptStuff(Apple) //error: type mismatch; |
Receiving an instance of the case class is much safer than receiving Any. However, sometimes you don’t have control over this. For example, when passing messages to actors, you can’t control what is received in a type-safe manner at compile time. So, use caution when passing around case classes.
Although the Scala compiler may evolve to fix the previous problem, these kinds of edge cases can still arise. This emphasizes the need for good testing even in a statically typed language (see Chapter 16, Unit Testing).
3.144.93.73