'Scala - Using a Match Expression Like a switch Statement'에 해당되는 글 1건

  1. 2014.03.28 Scala - Using a Match Expression Like a switch Statement
00.scala2014. 3. 28. 12:54
반응형

Problem: You have a situation where you want to create something like a simple Java integer-based switch statement, such as matching the days in a week, months in a year, and other situations where an integer maps to a result.

Solution

To use a Scala match expression like a Java switch statement, use this approach:

i match {
  case 1  => println("January")
  case 2  => println("February")
  case 3  => println("March")
  case 4  => println("April")
  case 5  => println("May")
  case 6  => println("June")
  case 7  => println("July")
  case 8  => println("August")
  case 9  => println("September")
  case 10 => println("October")
  case 11 => println("November")
  case 12 => println("December")
  // catch the default with a variable so you can print it
  case whoa  => println("Unexpected case: " + whoa.toString)
}

That example shows how to take an action based on a match. A more functional approach returns a value from a match expression:

val month = i match {
  case 1  => "January"
  case 2  => "February"
  case 3  => "March"
  case 4  => "April"
  case 5  => "May"
  case 6  => "June"
  case 7  => "July"
  case 8  => "August"
  case 9  => "September"
  case 10 => "October"
  case 11 => "November"
  case 12 => "December"
  case _  => "Invalid month"  // the default, catch-all
}

The @switch annotation

When writing simple match expressions like this, it’s recommended that you use the @switchannotation. This annotation provides a warning at compile time if the switch can’t be compiled to atableswitch or lookupswitch.

Compiling your match expression to a tableswitch or lookupswitch is better for performance, because it results in a branch table rather than a decision tree. When a value is given to the expression, it can jump directly to the result rather than working through the decision tree.

Here’s the official description from the @switch annotation documentation:

“An annotation to be applied to a match expression. If present, the compiler will verify that the match has been compiled to a tableswitch or lookupswitch, and issue an error if it instead compiles into a series of conditional expressions.”

The effect of the @switch annotation is demonstrated with a simple example. First, place the following code in a file named SwitchDemo.scala:

// Version 1 - compiles to a tableswitch
import scala.annotation.switch

class SwitchDemo {

  val i = 1
  val x = (i: @switch) match {
    case 1  => "One"
    case 2  => "Two"
    case _  => "Other"
  }

}

Then compile the code as usual:

$ scalac SwitchDemo.scala

Compiling this class produces no warnings, and creates the output file SwitchDemo.class. Next, disassemble that file with this javap command:

$ javap -c SwitchDemo

The output from this command shows a tableswitch, like this:

16:  tableswitch{ //1 to 2
            1: 50;
            2: 45;
            default: 40 }

This shows that Scala was able to optimize your match expression to a tableswitch. (This is a good thing.)

Next, make a minor change to the code, replacing the integer literal 2 with a value:

import scala.annotation.switch

// Version 2 - leads to a compiler warning
class SwitchDemo {

  val i = 1
  val Two = 2                     // added
  val x = (i: @switch) match {
    case 1  => "One"
    case Two => "Two"             // replaced the '2'
    case _  => "Other"
  }

}

Again, compile the code with scalac, but right away you’ll see a warning message:

$ scalac SwitchDemo.scala 

SwitchDemo.scala:7: warning: could not emit switch for @switch annotated match
  val x = (i: @switch) match {
               ^
one warning found

This warning message is saying that neither a tableswitch nor lookupswitch could be generated for the match expression. You can confirm this by running the javap command on the SwitchDemo.class file that was generated. When you look at that output, you’ll see that the tableswitch shown in the previous example is now gone.

In his book, Scala In Depth, Joshua Suereth states that the following conditions must be true for Scala to apply the tableswitch optimization:

  1. The matched value must be a known integer.
  2. The matched expression must be “simple.” It can’t contain any type checks, if statements, or extractors.
  3. The expression must also have its value available at compile time.
  4. There should be more than two case statements.

For more information on how JVM switches work, see the Oracle document, “Compiling Switches.”

Discussion

As demonstrated in other recipes, you aren’t limited to matching only integers; the match expression is incredibly flexible:

def getClassAsString(x: Any):String = x match {
  case s: String => s + " is a String"
  case i: Int => "Int"
  case f: Float => "Float"
  case l: List[_] => "List"
  case p: Person => "Person"
  case _ => "Unknown"
}

Handling the default case

The examples in the Solution showed the two ways you can handle the default, “catch all” case. First, if you’re not concerned about the value of the default match, you can catch it with the UNDERSCORE wildcard:

case _ => println("Got a default match")

Conversely, if you are interested in what fell down to the default match, assign a variable name to it. You can then use that variable on the right side of the expression:

case default => println(default)

Using the name default often makes the most sense and leads to readable code, but you can use any legal name for the variable:

case oops => println(oops)

You can generate a MatchError if you don’t handle the default case. For instance, given this matchexpression:

i match {
  case 0 => println("0 received")
  case 1 => println("1 is good, too")
}

If i is a value other than 0 or 1, the expression throws a MatchError:

scala.MatchError: 42 (of class java.lang.Integer)
  at .<init>(<console>:9)
  at .<clinit>(<console>)
    much more error output here ...

So unless you’re intentionally writing a partial function, you’ll want to handle the default case. (See Recipe 9.8, “Creating Functions that Work for a Subset of Data (Partial Functions),” for more information on partial functions.)

Do you really need a switch statement?

Of course you don’t really need a switch statement if you have a data structure that maps month numbers to month names. In that case, just use a Map:

val monthNumberToName = Map(
    1  -> "January",
    2  -> "February",
    3  -> "March",
    4  -> "April",
    5  -> "May",
    6  -> "June",
    7  -> "July",
    8  -> "August",
    9  -> "September",
    10 -> "October",
    11 -> "November",
    12 -> "December"
)

val monthName = monthNumberToName(4)
println(monthName)  // prints "April"


Posted by 1010