Matching Using case Classes

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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.93.73