14a. Case Classes - RobertMakyla/scalaWiki GitHub Wiki

KEY POINTS:

  • case class is a class for which the compiler automatically produces the methods that are needed for pattern matching (apply / unapply)
  • The common superclass in a case class hierarchy should be sealed (zaplombowana / zapieczętowana), so that they are not extended outside file.
  • Use the Option type for values that may or may not be present — it is safer than using null !!!

Case Classes - normal classes optimized to use for pattern matching

         abstract class Amount

         case class Dollar(value: Double) extends Amount                   // value is automatically 'val'
         case class Currency(value: Double, unit: String) extends Amount   // value is automatically 'val'

         case object Nothing extends Amount    //case object - for singletons

         val amt:Amount = Dollar(5.5)    // all case classes have by default companion objects with apply()

         amt match {
             case Dollar(v) => "$" + v                 // 'v' becomes 'val', unless we declare it 'var'
             case Currency(_, u) => "I got " + u      // 'u' becomes 'val', unless we declare it 'var'
             case Nothing => ""
         }

All Case Classes have companion object with apply() so that there's no need to use 'new' while instantiating.

All Case Classes have companion object with unapply() to make pattern matching work

Methods toString, equals, hashCode, and copy -if not provided explicitly - they are generated automatically

Generated methods: equal and hashcode use all items from constructor :-)

Apart from that, Case Classes are just like other classes

The copy Method

 Makes another object with the same values

     val amt = Currency(29.95, "EUR")
     val price = amt.copy()

 Just like that it is not very useful. The amt and it's properties are immutable (val)
 so I can share object's reference with no worries

 It gets useful than I want to change some values:

     val price = amt.copy(value = 19.95) // Currency(19.95, "EUR")

 Note: when we use copy() with not arguments: copy() returns the same reference (the same object)
 Note: when we change something, copy(value=10) returns the different object with different value

Infix Notation in case Clauses

 This feature is for Case Classes that take exactly 2 params. Here is silly example:

     amt match {
         case a Currency u => ...  // Exactly the same as: case Currency(a, u)
     }

 More advanced example - first param is Int (head), second is List[Int] (tail):

     case class :: (head: Int, tail: List[Int])

     val lst = ::( 1, List(2,3,4) )

     lst match {
         case h :: t => println("head: " + h + "; tail: " + t)  // Same as case ::(h, t), which calls ::.unapply(lst)
     }

 *** Re-defining module '+:'

     case object +: {
         def unapply[T](input: List[T]) = if (input.isEmpty) None else Some((input.head, input.tail))
     }

     11 +: 7 +: 2 +: 9 +: 8 +: 3 +: Nil           // we get  List(11, 7, 2, 9, 8, 3)

     11 +: 7 +: 2 +: 9 +: 8 +: 3 +: Nil match {
         case first +: second +: rest => println("first: "+ first + ", second:"+ second + ", rest "+ rest )
     }
                                                  // we get first: 11, second:7, rest List(2, 9, 8, 3)

The '@' sign

 We use '@' only when we want to deal with the object itself, and not only it's content. Hence:

     case class Person(name: String, age: Int)

     somePerson match {
         case p @ Person(_, age) if p != bill => "That " + age + " y.o. person ain't bill"
         case Person(_, age) => "That person is " + age + " y.o."
         case _ => println("Not a person")
     }

Matching Nested Structures

 {
     trait Item {
         def calculate: Double = this match {
             case Article(_, price) => price
             case Pack(_, disc, items @ _*) => items.map(_.calculate).sum - disc
         }
     }
     case class Article(description: String, price: Double) extends Item
     case class Pack(description: String, discount: Double, items: Item*) extends Item


     val pack = Pack("Father's day special", 20.0,
                   Article("Scala for the Impatient", 39.95),
                   Pack("Anchor Distillery Sampler", 10.0,
                      Article("Old Potrero Straight Rye Whiskey", 79.95),
                      Article("Junípero Gin", 32.95)))

     pack match {
         case Pack(_, _, Article(descr, _), _*) => println("The descr should be name of this book: " + descr)
     }         //  |  |            |    |   |
               //  |  |            |    |   |
               //  |  |            |    |   other items in pack
               //  |  |            |    article price
               //  |  |            article description
               //  |  pack discount
               //  pack description
 }

 I can bind a nested value to a variable, using the @ notation:

     case Pack(_, _, art @ Article(_, _), rest @ _*) => .../* use art, rest*/

 Function that calculates price of whole pack:

WHICH APPROACH IS BETTER - OO vs Functional :

OO approach : calculate() could be abstract method of the superclass, each subclass could override it.

OO+ Scala-  : If we add many subclasses of Pack, then OO approach is better,
              because all we have to do is write another subclass and it's own implementation of price()

              Scala approach would require writing subclass but also update superclass method :/

OO- Scala+  : Case classes work well if we don't change much the structure (no new subclasses) but we create a lot of methods
              Then we create these methods in 1 place: one super-trait

              Pattern matching often leads to more concise code than inheritance.
              It is easier to read compound objects that are constructed without new.
              You get toString, equals, hashCode, and copy for free.

Unattended Test Note: We can have 2 classes: Item and a trait/class Operation which will have method eval(s:String):f more subclasses of Operation are not really needed cause each operation takes 2 items and returns a function. so we will just be adding cases in Operation.eval(s:String):f method.

                   or we could have an object with 1 method taking a String and returning a function

Sealed (zaplombowane/ zapieczętowane)

Super-types of case classes/objects should be sealed. Then if someone tries to extend is outside the file, it won't compile.

// in one file
  sealed trait Currency
  case object Dollar extends Currency
  case object Pound extends Currency

// in another file, with all the imports
  case object Rupee extends Currency // ERROR - illegal inheritance from sealed class

Option (supertype of None and Some)

The Option type in the standard library uses case classes to express values that might or might not be present

The case subclass 'Some' wraps a value, for example Some("Fred") The case object 'None' indicates that there is no value

val scores = Map("Alice"-> 1, "Fred"-> 2)

scores.get("Alice") match {   // Map.get() returns Option[T]
   case Some(score) => println(score)  // 'Score' binds score to Int
   case None => println("No score")
}

Function that returns Option[Double]

    def f(x: Double): Option[Double] = if (x >= 0) Some(math.sqrt(x)) else None

Partial Functions

The 'case' clause, is actually instance of PartialFunction[InputType, OutputType]. It has 2 functions:

.apply()          // computes value from matching pattern
.isDefinedAt()   // --> true if it matches the pattern

It's the same as:

  val f1: PartialFunction[Char, Int]                 = { case '+' => 1 ; case '-' => -1 }
  val f2_with_wildcard: PartialFunction[Char, Int]   = { case _ => 0 }

  f1('-')                         // Calls f.apply('-'), returns -1

  f.isDefinedAt('0')              // false
  f1('0')                         // Throws MatchError

Partial Function can be composed:

     val f_complete = f1 orElse f2_with_wildcard
     f_complete('+')  // --> 1
     f_complete('-')  // --> -1
     f_complete('a')  // --> 0