Scala - Case Classes and Pattern Matching


One of the means that  scala uses for branching the process based on the input is use of case classes and pattern matching, it is indeed a very advanced technique comparing to enum, but it has more rich context in case of pattern matching.. 

in this chapter, we will show and discuss somehting like case class (being sealed, exhaustive enumerable - so that compiler knows that no partial cases not covered, and etc..), different patterns (variable pattern, willdcard pattern, constant pattern, constructor pattern, sequence pattern, tuple pattern, typed pattern ) , other pattern assisting means such as pattern guard... we will then discuss "Sealed classes", pattern everywhere - like in for expression, the Option Type, pattern as partial function. Last we will present a live example with pattern matching..  (we might present this in another chapter) 

Introduction to Case classes

Where if you are already programmed against the functional programming language, you might already getting to know pattern matching, while case classes is the scala way to allow pattern matching on object without requiring a large amount of boilerplate code. 

first let see an simple example. we will write a library to manipulate arithmetic expressions, perhaps as part of domain-specific language you are designing. 

// simple_case_example.scala

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
what you will get from the case classes?

1. free factory method on the companion object

// what the case class has ?
// factory method in the companion object
val v = Var("x")

this gives you the convenience on writting like this. 

val op = BinOp("+", Number(1), v)
2. quick access to the case classes members. 
// implicit member declaration 
// implicit prefix val to the constructor members
v.name
op.left

3. implicit/nature implemented toString methods
//adds a nature "natural" implementation of toString
println(op)
4. natural implemented equals methods
// equal method implemented implicitly in the companion object
op.right == Var("x")
5. free copy method
// and there is a copy method 
val copy = op.copy(operator = "-")

Pattern matching

simple case of pattern matching. 
def simplifyTop(expr : Expr) : Expr = expr match {
  case UnOp("-", UnOp("-", e)) => e
  case BinOp("+", e, Number(0)) => e
  case BinOp("*", e, Number(1)) => e
  case _ => expr
}
with this method 'simplyTop', we can simplify the operation somehow, so the basic syntax of the pattern matching is as follow. 

selector match { alternatives }

instead of the 

switch (selector) { alternatives }
so what we might get from the pattern matching that is different from the switch case one? we might get the following. 
  • match expression
  • match never fall through 
  • match throw exception on those partially matched cases (MatchError)
matching being a pattern expression, being a expression,means that it can return a value. match never fall through means that you don't need to write explicitly the break statement. and last, if the match pattern is not exhaustive, then it might throw a MatchError. 

Kinds of Patterns   

wildcard pattern

wildcard pattern (_) matches any objects whatsoever, you have already seen it as a default catch-al alternatives, such as .

// - wildcard pattern

expr match { 
  case BinOp(op, left, right) => println(expr + " is a binary operation")
  case _ => 
}
and it is not just the mere the pattern itself being _ can it be called a wildcard pattern, the wildcard notation _ can also appear in part of the pattern, such as the following. 
// - wildcard pattern 
//  wildcard pattern can also appear in part of the pattern 
expr match {
  case BinOp(_, _, _) => println(expr + " is a binary operation")
  case _ => println("It's something else")
}

while there is a gotach in the the following code. 

val expr : Expr = BinOp("+", Number(1), Var("x"))
val expr : Expr = UnOp("-", Number(1))
// if you write as follow 
// val expr =  UnOp("-", Number(1))
// you will get an error saying 
// <console>:18: error: constructor cannot be instantiated to expected type;
// found   : BinOp
// required: UnOp
//                case BinOp(op, left, right) => println(expr + " is a binary operation")
//                     ^
that is you can pattern match a Expr against a BinOp, but you cannot pattern match a BinOp against a UnOp class. 
Constant pattern
Constant pattern matches only itself, Any literal may be used as a constant. 
however, any val or singleton object can be used as a constant. 

e.g. of the constant pattern is like this:

// - constant pattern

def describe(x :Any) = x match {
  case 5 => "Five"
  case true => "True"
  case "hello" => "hi!"
  case Nil => "the empty list"
  case _ => "something else"
}

describe(5)
describe(Nil)
describe("hello")

as you can see, Nil is a constant - or a object. 
Variable Pattern

A variable pattern matches any object, just like a wildcard, unlike a wildcard, scala binds the variable to whatever the object is. 
you can then use this variable to act on the object further. 

// - variable pattern
expr match {
  case 0 => "zero"
  case somethingElse => "not zero:" + somethingElse // remember the variable pattern has to begin with a lower-case identifier name (more than convention)
}
however, we have to tell the difference from the constant pattern to the variable pattern. 
the main reaons is like that we can use symbolic names, you saw this already when we used a Nil as a pattern. A n exmplae .
import scala.math.{E, Pi}

E match { 
  case Pi => "strange math?  Pi = " + pi
  case _ => "OK"
}
however, if we assign a new val value pi and initialize the value of pi to value of Pi, how does this look s like?
val pi = Pi

E match { 
  case pi =>  "Strange math? Pi = " + pi
  // this is not allowed , compile time error 
  // case _ =>
}
you will see that the "pi" wil catch all selectors, so there is no case that can fall through to the _ pattern.

so, how the scala disambiguate the constant pattern from the variable pattern? actually Scala uses a very simple lexical rule, where a simple name starting with a lowercase letter is taken to be a pttern variable, all other references are taken to be constants. 

however, what can we do if we want to use the constant value where the value lies in a variable start with a lowercase letter. 
// two ways
E match { 
  case `pi` =>  "Strange math? Pi = " + pi
  // this is not allowed , compile time error 
  // case _ =>
  case _ => "OK"
}
or you can prefix it with some qualifier, such as if pi is a variable pattern, but this.pi or obj.pi are constatns event though they start with a lowercase. 
//
class Dummy(val pi : Double)
val dummy = new Dummy(Pi)
E match { 
  case dummy.pi =>  "Strange math? Pi = " + pi // or something like this.pi
  // this is not allowed , compile time error 
  // case _ =>
  case _ => "OK"
}


Constructor pattern
Constructors are where pattern matching becomes really powerful, a construcor pattern like "BinOp("+", e, Number(0))", it consists of a name (BinOp) and then a number of within parenthesis : "+", e, and Number(0). 

assuming that the name designates a case class, such a pattern means to first check that the object is member of the named case classes, and then to check that the constructor paramters of the object match the extra patterns supplied. 

these extra patterns mean that Scala patterns support deep matches. Such patern not only check the top-level object supplied, but also check the contents of the object against futher patterns. 

// - constructor pattern
expr match { 
  case BinOp(op, left, right) => println(expr + " is a binary operation")
  case _ => 
}

Sequence pattern
You can match against sequence types like List or Array just like you match against case classes. Use the same syntax, but now you can specify any number of elements the pattern. 
example is as follow. 
// sequence pattern

expr match {
  case List(0, _, _) => println("found it!") // a list of three length, first being 0
  case _ => 
}

you can also have the repeated sequence pattern how long it can be. 

e.g.

expr match {
  case List(0, _*) => println("found it!") // don't care how long the list are..
  case _ => 
}
as you can see, once you are not sure how long the sequence might be, you can use the _*, the same pattern has bee used in the parameter declaration for the variable length parameter.. 


Tuple pattern
you can match against tuple too. A pattern like (a, b, c) matches an arbitrary 3-tuple. 
example as follow. 
// - tuple pattern 
def tupleDemo(expr : Any) = 
expr match {
  case (a, b, c) => println("matched " + a + b + c)
  case _ => 
}

tupleDemo(("a " , 3, "-tuple"))
Typed pattern
you can use a typed pattern as a convenient replacement for type tests and type casts. (think if without the typed pattern - you have to do all the casting and checking) 

the following show type pattern. 

// - type pattern
def generalSize(x : Any) = x match { 
  case s :String => s.length
  case m : Map[_, _] => m.size // careful of the type erasure
  case _ => -1
}
generalSize("abc")
generalSize(Map( 1 -> 'a', 2 -> 'b'))
well, I mentioned that without the typed pattern match, we have to resort to the following type of code , you will use something as follow. 
// if without the typed pattern, you will need to 
//  expr.isInstanceOf[String]
//  expr.asInstanceOf[String]
// 

if (x.isInstanceOf[String]) {
  val s = x.asInstanceOf[String]
  s.length
} else {
  
}
type erasure
this is not a problem of java rather than a problem of scala, let's first see an example as follow. 
// type erasure and type match  

def isIntIntMap(x : Any) = x match { 
  case m : Map[Int, Int] => true
  case _ => false
}
isIntIntMap(Map(1 -> 1, 2 -> 2))
isIntIntMap(Map("abc" -> "abc", "ab" -> "ab")) // return true

if  you compile, you might get an warning, saying that        

"non variable type-argument Int in type pattern is unchecked since it is eliminated by erasure case m: Map[Int, Int] => true "

as you can see, that if you pass "Map[String, String]" to the isIntMap, which expect and check a Map[Int, Int].
While, java has  type erasure on generics, but the only exception to the erasure rule is arrays, because they are handled specially in Java as well in scala.

// type erasure exception is Arrays
def isStringArray(x :Any) = x match { 
  case a : Array[String] => "Yes"
  case _ : => "no"
}
val as = Array("abc")
isStringArray(as)

val ai = Array(1, 2, 3)
isStringArray(as)
Variable  Binding
In addition to be standard variable patterns, you can also add a variable in any other pattern, you simply write the variable name, an @ sign, and then in the pattern is to perform match as normal, and if the pattern succeeds, set the variable to the matching object just as  with a simple variable pattern.
// - pattern guards
//   why ?

def simplify(e : Expr) = e match { 
  case BinOp("+", x, x) => BinOp("*", x , Number(2))
  case _ => e
}
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 operation such as e + e by multiplications of two, e.g. e * 2,,, 

e.g if you want to transfer the following 

BinOp("+", Var("x"), Var("x"))

to this

BinOp("*", Var("x"), Number(2))
and you might be tempted to write as follow. 
// Pattern Guards
//  why you need to write the pattern guards?
// -- this is not allowed
def simplyAdd(e : Expr) = e match { 
  case BinOp("+", x, x) => BinOp("*", x, Number(2))
  case _ => e
}
// because scala pattern only allow pattern to be linear...
and with the Pattern Guard, you can do like this:

// because restrict pattern to be linear : a pattern variable only appear once in a pattern 
// pattern guard gives you a way to guard the pattern with an if 
def simplify(e : Expr) = e match { 
  case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2))  
  case _ => e
}
Pattern Overlapds
patterns are tried in order in which they are written. exaple is as follow. 
// - pattern overlaps

// a rule of thumb, always put the more specialized rule first and then those more general rules.
def simplifyAll(e : Expr) : Expr = e match { 
  case UnOp("-", UnOp("-", e)) => simplifyAll(e)
  case BinOp("+", e, Number(0)) => simplifyAll(e)
  case BinOp("*", e, Number(1)) =>  simplifyAll(e)
  case UnOp(op, e) => UnOp(op, simplifyAll(e))
  case BinOp(op, l, r) => BinOp(op, simplifyAll(l), simplifyAll(r))
  case _ => expr
}

one important part of the pattern matches is that you would probably write more general cases such as the catch-call statement after the more specific ones. In most cases, the compiler will even complain even if you tried. (however, you should leverage the compiler's intelligence, but you should be careful when you write the pattern cases)

Sealed Classes

Whenever you write a pttern match , you need to make sure you have convered  all of hte possible cases. Sometimes you can do this by adding a default case at the end of the match..

however, how can you be so sure that all cases are covered? 

Scala compiler can enlist the help to you, however, in order to use this ability, you will need a way to tell scala which are possible classes. because think of this way, there is no way to prevent you from adding new classes.
the alternatives is to make superclasses of your case classes sealed. A sealed class cannot have any new subclasses added except the ones in the need to worry about the subclasses you already know about. what's more you get is better compiler support. 

// simply put the 'sealed' keyword before the 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
now, if you try to write a pattern with some of the possible cases are left out. 
def describe(e : Expr): String = e  match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}
while you compiler,  you will get compiler erorr, something like :

warning : match is not exhausted!
missing combination: UnOp
missing combination: BinOp

so you will need to write this way.

// not ideal?
def describe(e : Expr): String = e  match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
  case _ => throw new RuntimeException // should not happen
}
however, you may not be very happy that you need to write exhaustive cases...  because sometimes you are pretty sure that you will only deal a few cases are covered, you can do is the @unchecked annotation to suppress the warning. 

// = @unchecked - shut the compiler up
def describe(e : Expr): String = (e: @unchecked)  match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}
Option Type
Scala has a standard type called Option for optional values, such a value can be of two forms, it ca be of the form Some(x) where x is the actual value, or it can be the None object, which represents the missing value. 

e.g.

// - option type 

val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals get "France"     // Option[String] = Some(Paris)
capitals get "North Pole" // Option[String] = None 

// option in match 
def show(x : Option[String]) = x match {
  case Some(s) => s 
  case None => "?"
}

show(capitals get "France" )
show(capitals get "North Pole")

Pattern everywhere

you can use patterns nearly everywhere, such as the the following tuple pattern in the assignment elow. 

// tuple pattern in assignment
val myTuple = (123, "abc")
val (number,  string)  = myTuple


// constructor in the assignmnet  
val exp = new BinOp("*", Number(5), Number(1))
val BinOp(op, left, right) = exp


// op: String = *
// left : Expr = Number(5.0)
// right: Expr = Number(1.0)

case sequence as partial functions

what is called a partial functions. 

A sequence of cases (i.e. alternatives) in curly braces can be used anywhere a function literal can be used. 

an example of the partial fucntion is as follow. 

// e.g. of partial function is actors library
react { 
  case (name : String, actor : Actor) => {
    actor ! getip(name)
    act()
  }
  case msg => {
    println("Unhandled message: " + msg)
    actor()
  }
}
so, a sequence gives you a partial function. 
// one other generalizing is worth noting, a sequence gives you you a partial function. 
// 
val second :: List[Int] => Int = {
  case x :: y :: _ => y
}
however, the type 
List[Int] => Int

include all functions from List[Int] to Int.. while partial functions are only partial of the List[Int] => Int.. The type of the parital function is 

ParitalFunction[List[Int],Int]
and the new type definition is as follow. 
// get into the internals
new PartialFunction[List[Int], Int] { 
  def apply(xs : List[Int]) = xs match { 
    case x :: y :: _ => y
  }
  
  def isDefinedAt(xs : List[Int]) = xs match {
    case x :: y :: _ => true
    case _ => false
  }
}
and you can tell if a particular value is defined at certain input by this:
second.isDefinedAt(List(5, 6, 7)) // return true
second.isDefinedAt(List()) // return false
You might be interested in the following. the following is something that might be generated internally for the partial functions. 
// get into the internals
new PartialFunction[List[Int], Int] { 
  def apply(xs : List[Int]) = xs match { 
    case x :: y :: _ => y
  }
  
  def isDefinedAt(xs : List[Int]) = xs match {
    case x :: y :: _ => true
    case _ => false
  }
}

Patterns in for expresions

First let's see an example of the for expression on the dictionary type. 

val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo" )
// Patterns for the 'for' expressions
for ((country ,city ) <- capitals) { // use of the tuple pattern  
  println("The capital of " + country + " is " + city)
}
and it is possible that generated values does not match the pattern ...

// what if the generated value does not match the pattern?
// 
val results = List(Some("apple"), None, Some("orange"))

for (Some(fruit) <- results ) println(fruit)

A large example

we want to dispaly something as following.

x 
--------
 x + 1
while we can leverage the element class that we have developed before (in the chapter of elements and etc...)


and we will need to deal with the expression ourselves, how to deal with it??? we have learned how to use the match-case expressions in this post, let's apply it. 

// pattern_examples.scala
//

// description:
//  will demonstrate the use of the pattern matching.


package org.stairwaybook.expr
import org.stairwaybook.layout.Element.elem // the element library 
import org.stairwaybook.layout.Element

sealed abstract class Expr
case class Number(val n : Double) extends Expr
case class Var(val name : String) extends Expr
case class UnOp(val op : String, arg : Expr) extends Expr
case class BinOp(val op : String, left : Expr, right : Expr) extends Expr

class ExprFormatter {
  // contains operator sin groups of  increasing precedance
  private val opGroups = Array(
    Set("|", "||"),    
    Set("&", "&&"),
    Set("^"),
    Set("==", "!="),
    Set("<", "<=", ">", ">="),
    Set("+", "-"),
    Set("*", "%")
  )
  
  // A mapping from operators to their precedance
  private val precedence = {
    val assocs =  // create the maps from the association to map
      for {
        i <- 0 until opGroups.length
        op <- opGroups(i)
      } yield (op -> i)
      
      assocs.toMap
  }
  
  private val unaryPrecedence = opGroups.length
  private val fractionPrecedence = -1

  
  private def format(e : Expr, enclPrec : Int) : Element = 
    e match { 
    case Var(name) => elem(name)
    case Number(num) => 
      def stripDot(s : String) =
        if (s endsWith ".0") s.substring(0, s.length - 2)
        else s
      elem(stripDot(num.toString))
    case UnOp(op, arg) => 
      elem(op) beside format(arg, unaryPrecedence)
    case BinOp("/", left, right) => 
      val top = format(left, fractionPrecedence)
      val bot = format(right, fractionPrecedence)
      val line = elem('-', top.width max bot.width, 1)
      val frac = top above line above bot
      if (enclPrec != fractionPrecedence) frac
      else elem(" ") beside frac beside elem(" ")
    case BinOp(op, left, right) =>
      val opPrec = precedence(op)
      val l = format(left, opPrec)
      val r = format(right, opPrec + 1)
      val oper = l beside elem(" " + op + " " ) beside r
      if (enclPrec <= opPrec) oper
      else 
        elem("(") beside oper beside elem(")")
  }
  
  // overload method need the return type
  def format(e : Expr)  :Element = format(e, 0)
}
and we can write some test code to test the code we have written above. 
// pattern_examples_application.sclaa
//
// description: 
//  this is the application drive for the pattern_example.scalaa

import org.stairwaybook.expr._
import org.stairwaybook.expr.ExprFormatter

object Express extends Application { 
  val f = new ExprFormatter
  val e1 = BinOp("*", BinOp("/", Number(1), Number(2)),
      BinOp("+", Var("x"), Number(1))
      )
  val e2 = BinOp("+", BinOp("/", Var("x"), Number(2)),
      BinOp("/",  Number(1.5), Var("x"))
      )  
      
   val e3 = BinOp("/", e1, e2)
   
   def show(e: Expr) = println(f.format(e) + "\n\n")
   
   for (e <- Array(e1, e2, e3)) show(e)
}

and the output could be something like this:  
1
-  * (x + 1)
2
and for those of you how is interested to know the element class's code, you can find it below. 

// stairwaybook_elements.scala

// description:
//   a show case on the composition and inheritance

package org.stairwaybook.layout

object Element { 
  
  private class ArrayElement(val contents: Array[String]) extends Element 

  
  private class LineElement(
      s : String) extends Element {
          def contents : Array[String] = Array(s)
          override def height : Int = 1
          override def width : Int = s.length
    }
  
  private class UniformElement(
      chr : Char,
      override val width : Int,
      override val height :Int
      ) extends Element {
    
    // ch * width will fail, 
    // and the error message is not clear - type mismatch (expect String, Int provided) the REPL need to improve 
    // on it error handling.
    private val line : String = chr.toString * width
    override def contents : Array[String] = Array.fill(height)(line)
  }
  
  
  def elem(contents: Array[String]) : Element = new ArrayElement(contents)
  
  def elem(s : String) : Element  = new LineElement(s)
  
  def elem(chr : Char, width : Int, height : Int) : Element= new UniformElement(chr, width, height)
  
}


import Element.elem

// contents: not defined, you need to declare your class as "abstract" 
//  
abstract class Element { 
  def contents : Array[String]
  def height : Int = contents.length
  def width = if (contents.length ==0 ) 0 else contents(0).length
  
  
  def beside(that : Element) : Element = {
    val this1 = this heighten that.height
    val that1 = that heighten this.height
    
    elem (
      for ((line1, line2) <- this1.contents zip that1.contents) yield (line1 + line2)    
    )
    
  }
  
  
  def above(that : Element) : Element = {
    val this1 = this widen that.width
    val that1 = that widen this.width
    
    elem(this1.contents ++ that1.contents)
    
  }
  
  def widen(w : Int) : Element = 
    if (w <= width) this
    else {
      val left = elem(' ', (w - width) /2 , height)
      val right = elem(' ', (w - width  + left.width), height)
      
      left beside this beside right
    }
  
  def heighten(h : Int) : Element = 
    if (h <= height) this 
    else {
      val top = elem(' ', width, (h - height) / 2)
      val bottom = elem(' ', width, (h - height + top.height))
      
      top above this above bottom
    }
    
  
  override def toString = contents mkString "\n"
    
}






转载于:https://my.oschina.net/u/854138/blog/132796

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值