(8) Case Classes and Sealed Types

(8) Case Classes and Sealed Types

About

:octocat: GitHub: All of the example code: repo (link)

:page_facing_up: blog link: https://purrgramming.life/cs/programming/fp/ :star:


Case Classes

Case classes are like regular classes with a few key differences, which we will go over. Case classes are good for modelling immutable data.

Defining a case class

A minimal case class requires the keywords case class, an identifier, and a parameter list (which may be empty):

case class Book(isbn: String)
val frankenstein = Book("978-0486282114")

Notice how the keyword new was not used to instantiate the Book case class. This is because case classes have an apply method by default which takes care of object construction.

When creating a case class with parameters, the parameters are public vals.

case class Message(sender: String, recipient: String, body: String)  

val message1 = Message("sender", "recipient", "body: hi")  

message1.sender // val res0: String = sender  

// this line does not compile  
// Error msg:   |Reassignment to val sender  
message1.sender = "[email protected]"

You can’t reassign message1.sender because it is a val (i.e. immutable). It is possible to use vars in case classes, but this is discouraged.

Comparison

Instances of case classes are compared by structure and not by reference:

val message1 = Message("sender", "recipient", "body: hi")  
val message2 = Message("sender", "recipient", "body: hi")  
message1 == message2 // val res1: Boolean = true

Even though message1 and message2 refer to different objects, the value of each object is equal.

Copying

You can create a (shallow) copy of an instance of a case class simply by using the copy method. You can optionally change the constructor arguments.

val message3 = message1.copy(sender = "sender2")  
message3 // val res2: Message = Message(sender2,recipient,body: hi)

Advantage

The main advantages of using case classes instead of classes are

  1. We can use pattern matching to access the arguments of the constructor that created an object
    We can get a reference to the arguments of the constructor.
    For example in

def eval(e: Expr): Int = e match

case  Number(n) => n

case  Sum(e1, e2) => eval(e1) + eval(e2)
  1. The compiler creates accessors for the attributes – The compiler creates a field accessor for each field of the case class

Not because

We are NOT using case classes instead of classes.

[F] Case classes can extend traits while classes cannot
[T] Both of them can

[F] use type tests in ‘match’ expressions to evaluate different expressions according to the runtime type of the value you “match” on
[T] Type tests can be used with both classes and case classes

Sealed Types

Intro

Scala’s final modifier makes a class or trait unavailable for an extension. Therefore, it’s quite restrictive. On the other hand, making a class public allows any other class to extend from it.

What if we want something in between these two? The sealed modifier in Scala comes to the rescue!

The sealed keyword controls the extension of classes and traits. Declaring a class or trait as sealed restricts the definition of its subclasses — we have to define them in the same source file.

package week2  

sealed abstract class MultipleChoice  

case class OptionA() extends MultipleChoice  

case class OptionB() extends MultipleChoice  

case class OptionC() extends MultipleChoice  

abstract class MultipleChoiceSub extends MultipleChoice

If we try to extend a sealed trait outside of the parent class file, the compiler will throw an exception with a message:


illegal inheritance from sealed class. 

Error:(5, 32) illegal inheritance from sealed class MultipleChoice
case class OptionD() extends MultipleChoice

However, there is no restriction in extending from a subclass of a sealed class:

import week2.MultipleChoice  
import week2.MultipleChoiceSub  

// Error:  Cannot extend sealed class MultipleChoice in a different source file  
case class OptionD() extends MultipleChoice  

case class OptionY() extends MultipleChoiceSub  //This is valid

Motivation

Using sealed classes, we can guarantee that only subclasses defined in the file exist. This helps the compiler knows all the subclasses of the sealed class.

Example

The compiler can emit warnings in case the match cases are not exhaustive, to prevent a MatchError exception. Let’s try to omit OptionC from our pattern match and observe the compiler behavior:

def selectOption(option: MultipleChoice): String = option match {
  case optionA: OptionA => "Option-A Selected"
  case optionB: OptionB => "Option-B Selected"
}

We get a nice warning message from the compiler because of non-exhaustive match cases:

Warning:(11, 54) match may not be exhaustive.
It would fail on the following input: OptionC()
  def selectOption(option: MultipleChoice): String = option match {

Leave a Reply

Your email address will not be published. Required fields are marked *