Case classes & pattern matching
1. What is case class
Case classes are Scala's way to allow pattern matching on objects without requiring a large amount of boilerplate. Generally, all you need to do is add a single case keyword to each class that you want to be pattern matchable.
2. Simple example.
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
in Scala you can leave out the braces around an empty class body if you wish, so class C is the same as class C {}
instead of abstract class we also can use Trait, https://www.geeksforgeeks.org/difference-between-traits-and-abstract-classes-in-scala/#:~:text=Abstract%20class%20contain%20constructor%20parameters,Traits%20are%20stackable.
3. Show code, class hierarchy.
4. Case class, :
The other noteworthy thing about the declarations of above code is that each subclass has a case modifier. Classes with such a modifier are called case classes. Using the modifier makes the Scala compiler add some syntactic conveniences to your class.
Benefits:-
a.) It adds a factory method with the name of the class. This means that, for instance, you can write Var("x") to construct a Var object, instead of the slightly longer new Var("x"):
The factory methods are particularly nice when you nest them:
val op = BinOp("+", Number(1), v)
b.) all arguments in the parameter list of a case class implicitly get a val prefix, so they are maintained as fields:
scala> op.left
res1: Expr = Number(1.0)
c.) The compiler adds "natural" implementations of methods toString, hashCode, and equals to your class. Since == in Scala always delegates to equals, this means that elements of case classes are always compared structurally: scala> println(op)
BinOp(+,Number(1.0),Var(x))
d.) The compiler adds a copy method to your class for making modified copies. This method is useful for making a new instance of the class that is the same as another one except that one or two attributes are different. The method works by using named and default parameters:
op.copy(operator = "-")
Best part of case class is it can use in patter matching.
Example :
def simplifyTop(expr: Expr): Expr = expr match {
case UnOp("-", UnOp("-", e)) => e // Double negation
case BinOp("+", e, Number(0)) => e // Adding zero
case BinOp("*", e, Number(1)) => e // Multiplying by one
case _ => expr
}
selector match { alternatives }
A pattern match includes a sequence of alternatives, each starting with the keyword case. Each alternative includes a pattern and one or more expressions, which will be evaluated if the pattern matches. An arrow symbol => separates the pattern from the expressions. A match expression is evaluated by trying each of the patterns in the order they are written. The first pattern that matches is selected, and the part following the arrow is selected and executed.
UnOp("-", UnOp("-", e)):-
A constructor pattern looks like UnOp("-", e). This pattern matches all values of type UnOp whose first argument matches "-". Its second argument will be bound to the name e. Note that the arguments to the constructor are themselves patterns. This allows you to write deep patterns using a concise notation.
Compare to Java's Switch(3 diffs).
a.) match is an expression in Scala (i.e., it always results in a value), while that's not the case in Java.
b.) Scala's alternative expressions never "fall through" into the next case so no break is require.
c.) If none of the patterns match, an exception named MatchError is thrown. This means you always have to make sure that all cases are covered, even if it means adding a default case where there's nothing to do. expr match {
case BinOp(op, left, right) =>
println(s"$expr is a binary operation")
case _ =>
}
Patterns:-
1.) wildcard pattern (_) :
The wildcard pattern (_) matches any object whatsoever. You have already seen it used as a default, catch-all alternative.
Wildcards can also be used to ignore parts of an object that you do not care about.
case BinOp(_, _, _) => println(s"$expr is a binary operation")
does not actually care what the elements of a binary operation are; it just checks whether or not it is a binary operation.
2.) Constant patterns
A constant pattern matches only itself.
def describe(x: Any) = x match {
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _ => "something else"
}
In above example 5, true, and "hello" are all constant patterns. Also, any val or singleton object can be used as a constant. For example, Nil, a singleton object, is a pattern that matches only the empty list.
3.) Variable patterns:- A variable pattern matches any object, just like a wildcard. But unlike a wildcard, Scala binds the variable to whatever the object is.
compiler will not even let you add a default case at all. Since variable is a variable pattern, it will match all inputs, and so no cases following it can be reached.
case foo => s"Hmm, you gave me a $foo"
4.) Constructor patterns:- this makes pattern matching really powerful.
Scala patterns support deep matches. Such patterns not only check the top-level object supplied, but also the contents of the object against further patterns. Since the extra patterns can themselves be constructor patterns, you can use them to check arbitrarily deep into an object.
case BinOp("+", e, Number(0)) => println("a deep match")
5.) Sequence patterns You can match against sequence types, like List or Array, just like you match against case classes.
a.) Fix length sequence pattern.case List(0, _, _)
b.) No length constraint: case List(0, _*)
6.) Tuple pattern: Tuple patterns You can match against tuples too.
case (a, b, c)
7.) Typed patterns: You can use a typed pattern as a convenient replacement for type tests and type casts.
def generalSize(x: Any) = x match { case s: String => s.length case m: Map[_, _] => m.size case _ => -1 }
"m: Map[_, _]" matches any value that is a Map of some arbitrary key and value types, and lets m refer to that value.
Type Eraser:-
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.
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.
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, which means the 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.
case UnOp("abs", e @ UnOp("abs", _)) => e
portion that matched the UnOp("abs", _) part is made available as variable e.
Pattern guards:-
case BinOp("+", x, y) if x == y =>
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.
All patterns are tried and executed in the order in which they are written.
Where else the pattern ?:
Patterns are everywhere in Scala. For examples:-
1.) In For loop-
This for expression retrieves all key/value pairs from the capitals map. Each pair is matched against the pattern (country, city), which defines the two variables country and city.
for ((country, city) <- capitals) println("The capital of " + country + " is " + city)
Generated values that do not match the pattern are discarded.
2.)
<<<<<< s.length case m: Map[_, _] => m.size case _ => -1 }>>>>>>>