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