ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 02.01.2026
Просмотров: 3481
Скачиваний: 0
Section 15.2 |
Chapter 15 · Case Classes and Pattern Matching |
322 |
scala> :quit
$ scala -unchecked
Welcome to Scala version 2.8.1
(Java HotSpot(TM) Client VM, Java 1.5.0_13). Type in expressions to have them evaluated. Type :help for more information.
scala> def isIntIntMap(x: Any) = x match { case m: Map[Int, Int] => true case _ => false
}
<console>:5: warning: non variable type-argument Int in type pattern is unchecked since it is eliminated by erasure
case m: Map[Int, Int] => true
ˆ
Scala uses the erasure model of generics, just like Java does. This means that no information about type arguments is maintained at runtime. Consequently, there is no way to determine at runtime whether a given Map object has been created with two Int arguments, rather than with arguments of different types. All the system can do is determine that a value is a Map of some arbitrary type parameters. You can verify this behavior by applying isIntIntMap to arguments of different instances of class Map:
scala> isIntIntMap(Map(1 -> 1)) res19: Boolean = true
scala> isIntIntMap(Map("abc" -> "abc")) res20: Boolean = true
The first application returns true, which looks correct, but the second application also returns true, which might be a surprise. To alert you to the possibly non-intuitive runtime behavior, the compiler emits unchecked warnings like the one shown above.
The only exception to the erasure rule is arrays, because they are handled specially in Java as well as in Scala. The element type of an array is stored with the array value, so you can pattern match on it. Here’s an example:
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.2 |
Chapter 15 · Case Classes and Pattern Matching |
323 |
scala> def isStringArray(x: Any) = x match { case a: Array[String] => "yes"
case _ => "no"
}
isStringArray: (x: Any)java.lang.String
scala> val as = Array("abc")
as: Array[java.lang.String] = Array(abc)
scala> isStringArray(as) res21: java.lang.String = yes
scala> val ai = Array(1, 2, 3) ai: Array[Int] = Array(1, 2, 3)
scala> isStringArray(ai) res22: java.lang.String = no
Variable binding
In addition to the standalone variable patterns, you can also add a variable to any other pattern. You simply write the variable name, an at sign (@), and then the pattern. This gives you a variable-binding pattern. The meaning of such a pattern is to perform the pattern match as normal, and if the pattern succeeds, set the variable to the matched object just as with a simple variable pattern.
As an example, Listing 15.13 shows a pattern match that looks for the absolute value operation being applied twice in a row. Such an expression can be simplified to only take the absolute value one time.
expr match {
case UnOp("abs", e @ UnOp("abs", _)) => e case _ =>
}
Listing 15.13 · A pattern with a variable binding (via the @ sign).
In Listing 15.13, there is a variable-binding pattern with e as the variable and UnOp("abs", _) as the pattern. If the entire pattern match succeeds, then the portion that matched the UnOp("abs", _) part is made available as variable e. As the code is written, e then gets returned as is.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.3 |
Chapter 15 · Case Classes and Pattern Matching |
324 |
15.3 Pattern guards
Sometimes, syntactic pattern matching is not precise enough. For instance, say you are given the task of formulating a simplification rule that replaces sum expressions with two identical operands such as e + e by multiplications of two, e.g., e * 2. In the language of Expr trees, an expression like:
BinOp("+", Var("x"), Var("x"))
would be transformed by this rule to:
BinOp("*", Var("x"), Number(2))
You might try to define this rule as follows:
scala> def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, x) => BinOp("*", x, Number(2)) case _ => e
}
<console>:11: error: x is already defined as value x
case BinOp("+", x, x) => BinOp("*", x, Number(2))
ˆ
This fails, because Scala restricts patterns to be linear: a pattern variable may only appear once in a pattern. However, you can re-formulate the match with a pattern guard, as shown in Listing 15.14:
scala> def simplifyAdd(e: Expr) = e match { case BinOp("+", x, y) if x == y =>
BinOp("*", x, Number(2)) case _ => e
}
simplifyAdd: (e: Expr)Expr
Listing 15.14 · A match expression with a pattern guard.
A pattern guard comes after a pattern and starts with an if. The guard can be an arbitrary boolean expression, which typically refers to variables in the pattern. If a pattern guard is present, the match succeeds only if the guard evaluates to true. Hence, the first case above would only match binary operations with two equal operands.
Some other examples of guarded patterns are:
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.4 |
Chapter 15 · Case Classes and Pattern Matching |
325 |
//match only positive integers case n: Int if 0 < n => ...
//match only strings starting with the letter ‘a’ case s: String if s(0) == 'a' => ...
15.4 Pattern overlaps
Patterns are tried in the order in which they are written. The version of simplify shown in Listing 15.15 presents an example where the order of the cases matters:
def simplifyAll(expr: Expr): Expr = expr match { case UnOp("-", UnOp("-", e)) =>
simplifyAll(e) |
// ‘-’ is its own inverse |
case BinOp("+", e, Number(0)) => |
|
simplifyAll(e) |
// ‘0’ is a neutral element for ‘+’ |
case BinOp("*", e, Number(1)) => |
|
simplifyAll(e) |
// ‘1’ is a neutral element for ‘*’ |
case UnOp(op, e) => |
|
UnOp(op, simplifyAll(e)) case BinOp(op, l, r) =>
BinOp(op, simplifyAll(l), simplifyAll(r)) case _ => expr
}
Listing 15.15 · Match expression in which case order matters.
The version of simplify shown in Listing 15.15 will apply simplification rules everywhere in an expression, not just at the top, as simplifyTop did. It can be derived from simplifyTop by adding two more cases for general unary and binary expressions (cases four and five in Listing 15.15).
The fourth case has the pattern UnOp(op, e); i.e., it matches every unary operation. The operator and operand of the unary operation can be arbitrary. They are bound to the pattern variables op and e, respectively. The alternative in this case applies simplifyAll recursively to the operand e and then rebuilds the same unary operation with the (possibly) simplified operand. The fifth case for BinOp is analogous: it is a “catch-all” case for arbitrary
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.5 |
Chapter 15 · Case Classes and Pattern Matching |
326 |
binary operations, which recursively applies the simplification method to its two operands.
In this example, it is important that the catch-all cases come after the more specific simplification rules. If you wrote them in the other order, then the catch-all case would be run in favor of the more specific rules. In many cases, the compiler will even complain if you try.
For example, here’s a match expression that won’t compile because the first case will match anything that would be matched by the second case:
scala> def simplifyBad(expr: Expr): Expr = expr match { case UnOp(op, e) => UnOp(op, simplifyBad(e)) case UnOp("-", UnOp("-", e)) => e
}
<console>:18: error: unreachable code
case UnOp("-", UnOp("-", e)) => e
ˆ
15.5 Sealed classes
Whenever you write a pattern match, you need to make sure you have covered all of the possible cases. Sometimes you can do this by adding a default case at the end of the match, but that only applies if there is a sensible default behavior. What do you do if there is no default? How can you ever feel safe that you covered all the cases?
In fact, you can enlist the help of the Scala compiler in detecting missing combinations of patterns in a match expression. To be able to do this, the compiler needs to be able to tell which are the possible cases. In general, this is impossible in Scala, because new case classes can be defined at any time and in arbitrary compilation units. For instance, nothing would prevent you from adding a fifth case class to the Expr class hierarchy in a different compilation unit from the one where the other four cases are defined.
The alternative is to make the superclass of your case classes sealed. A sealed class cannot have any new subclasses added except the ones in the same file. This is very useful for pattern matching, because it means you only need to worry about the subclasses you already know about. What’s more, you get better compiler support as well. If you match against case classes that inherit from a sealed class, the compiler will flag missing combinations of patterns with a warning message.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.5 |
Chapter 15 · Case Classes and Pattern Matching |
327 |
Therefore, if you write a hierarchy of classes intended to be pattern matched, you should consider sealing them. Simply put the sealed keyword in front of the class at the top of the hierarchy. Programmers using your class hierarchy will then feel confident in pattern matching against it. The sealed keyword, therefore, is often a license to pattern match. Listing 15.16 shows an example in which Expr is turned into a sealed class.
sealed abstract class Expr
case class Var(name: String) extends Expr case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String,
left: Expr, right: Expr) extends Expr
Listing 15.16 · A sealed hierarchy of case classes.
Now define a pattern match where some of the possible cases are left out:
def describe(e: Expr): String = e match { case Number(_) => "a number"
case Var(_) |
=> "a variable" |
}
You will get a compiler warning like the following:
warning: match is not exhaustive!
missing |
combination |
UnOp |
missing |
combination |
BinOp |
Such a warning tells you that there’s a risk your code might produce a MatchError exception because some possible patterns (UnOp, BinOp) are not handled. The warning points to a potential source of runtime faults, so it is usually a welcome help in getting your program right.
However, at times you might encounter a situation where the compiler is too picky in emitting the warning. For instance, you might know from the context that you will only ever apply the describe method above to expressions that are either Numbers or Vars. So you know that in fact no MatchError will be produced. To make the warning go away, you could add a third catch-all case to the method, like this:
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index