Mike Slinn

Sealed Classes and Extractors

— Draft —

Published 2014-01-12. Last modified 2016-10-18.
Time to read: 3 minutes.

Sealed classes and traits are introduced, and a section of the lecture discusses how to ensure that case class constructors are always subject to initialization logic.

This lecture expands on the material presented in the Pattern Matching lecture and shows how pattern matching builds on unapply to perform value extraction. Pattern matching on collections is deferred until the Pattern Matching on Collections lecture of the Intermediate Scala course.

The sample code for this lecture can be found in courseNotes/src/main/scala/SealedDemo.scala.

Sealed Classes and Traits Provide Exhaustive Matching

A sealed Scala class may not be directly subclassed by Scala code, unless the subclass is defined in the same source file as the sealed class. This can ensure that a match statement is exhaustive. Classes and traits can be decorated with the sealed attribute. Here is an example of a sealed Scala class.

Scala code
object SealedDemo extends App {
  sealed abstract class Color
  class White extends Color
  class Yellow extends Color
  class Blue extends Color
  class Green extends Color
  class Black extends Color
def colorSwatch(color: Color): Unit = color match { case color: White => println("White") case color: Yellow => println("Yellow") case color: Blue => println("Blue") case color: Green => println("Green") //case color: Black => println("Black") } // compiler will complain that Black was not tested for
println(colorSwatch(new Blue)) }

The compiler warning looks like this:

Compiler warning
[warn] /home/mslinn/work/course_scala_intro_code/courseNotes/src/main/scala/SealedDemo.scala:151: match may not be exhaustive.
[warn] It would fail on the following input: Black()
[warn]   def colorSwatch(color: Color): Unit = color match {

Extracting Values

Here is an example of how we can use pattern matching to extract values from case class instances. In this case, match uses the specified class’s unapply methods to see if the values can be extracted. If Frog.unapply and Dog.unapply both fail, the last case will match because it has no class or extraction. You could also specify an underscore instead of x, if you did not want to reference the value.

Scala code
case class Frog11(canSwim: Boolean, numLegs: Int, breathesAir: Boolean)
case class Dog3(name: String, barksTooMuch: Boolean)
case class Horse(name: String)
def extract(animal: Any): String = animal match { case Frog11(canSwim, numLegs, breathesAir) if numLegs>0 => s"Got a Frog11 with $numLegs legs; canSwim=$canSwim and breathesAir=$breathesAir"
case Frog11(canSwim, numLegs, breathesAir) => s"Got a tadpole without legs; breathesAir=$breathesAir"
case Dog3(name, barksTooMuch) => s"Got a Dog3 called $name and barksTooMuch=$barksTooMuch"
case x => s"Got an unexpected animal: $x" }
Scala code
scala> extract(new Dog3("Fido", barksTooMuch=false))
res0: String = Got a Dog3 called Fido and barksTooMuch=false
scala>
extract(new Dog3("Fifi", barksTooMuch = true)) res1: String = Got a Dog3 called Fifi and barksTooMuch=true
scala>
extract(new Frog11(canSwim=true, 0, breathesAir=false)) res2: String = Got a tadpole without legs; breathesAir=false
scala>
extract(new Frog11(canSwim=true, 4, breathesAir=true)) res3: String = Got a Frog11 with 4 legs; canSwim=true and breathesAir=true
scala>
extract(new Horse("Silver")) res4: String = Got an unexpected animal: Horse(Silver)

Notice that the output for the extract method is the same as for the classify method.

Exercise: Extractors vs. Matching types or values

We accomplished the same task two different ways. When might you want to use an extractor, and when might you simply want to match against types or values? What is the difference between the matched values?

Solution

Matching a variable against various types does not invoke unapply. Unapply is useful when you want to extract properties from an incoming object when a match occurs, or to parse an object into another type. The following code example contrasts the two approaches:

Scala code
object PMQuiz extends App {
  case class Frog12(canSwim: Boolean, numLegs: Int, breathesAir: Boolean)
val frog12 = Frog12(canSwim=true, numLegs=4, breathesAir=true)
frog12 match { // match by type only, unapply is not invoked case kermit: Frog12 => println(s"kermit=$kermit")
case other => println(other) }
frog12 match { // match by type and invoke unapply implicitly to extract properties as separate variables case Frog12(a, b, c) => println(s"Extracted properties are: canSwim=$a, numLegs=$b, breathesAir=$c")
case other => println(other) } }

You can run this example as follows:

Shell
$ sbt "runMain PMQuiz"
kermit=Frog12(true,4,true)
Extracted properties are: canSwim=true, numLegs=4, breathesAir=true 

Initialization Logic for Case Class Constructors

You may encounter the need to apply initialization logic when creating instances of a case class. An obvious way is to make the constructor private and providing a factory method in the companion object.

For instance, here is a case class called EMail that wraps a String, provided within the AbstractSealed1 console application. It also adds a few extra methods that are useful when working with email. This code uses the Java Pattern class for regular expressions.

Scala code
case class EMail(value: String) {
  import java.net.URLEncoder
def isValid: Boolean = EMail.emailRegex.matcher(value).find
def link(asCode: Boolean=true): String = s"${ if (asCode) "<code>" else "" }<a href=’mailto:$value’>$value</a>${ if (asCode) "</code>" else "" }"
/** Generates a mailto: link with the optional subject and/or body. * The subject and/or body will be URLEncoded. */ def mailTo(subject: String="", body: String=""): String = { import java.nio.charset.StandardCharsets.UTF_8 val queryString = if ((subject + body).trim.isEmpty) "" else "?" + (if (subject.trim.isEmpty) "" else "subject=" + URLEncoder.encode(subject.trim, UTF_8.toString)) + (if (subject.nonEmpty && body.nonEmpty) "&" else "") + (if (body.trim.isEmpty) "" else "body=" + URLEncoder.encode(body.trim, UTF_8.toString)) s"""mailto:${ link() }$queryString""" }
def validate: EMail = { assert(isValid) EMail(value.trim.toLowerCase) }
override def toString = validate.value }

We want to ensure that only valid EMail instances are created, so we provide the fromString factory method:

Scala code
object EMail {
  import java.util.regex.Pattern
val emailRegex = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE)
val empty = EMail("x@y.com")
def fromString(value: String) = new EMail(value).validate }

Now we can construct instances with fromString that behave as expected.

Shell
$ scala
Welcome to Scala 3.4.2 (11.0.23, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> import AbstractSealed1._
import AbstractSealed1._
scala>
val email1 = EMail.fromString("santa@claus.com") email1: AbstractSealed2.EMail = santa@claus.com

However, there remain two ways to create instances that do not invoke the desired initialization logic:

  1. Companion objects have factory methods called apply which are not masked when the primary constructor is marked private.
    Scala REPL
    scala> val email2 = EMail("santa@claus.com")
    email2: AbstractSealed1.EMail = santa@claus.com 
  2. As we learned in the Case Classes lecture, case classes have copy constructors.
    Scala REPL
    scala> val email3 = email1.copy("blah@ick.com")
    email3: AbstractSealed1.EMail = blah@ick.com 

The solution is a sealed abstract case class, provided in the AbstractSealed2 console application. We learned in the Classes Part 1 lecture that abstract classes cannot be instantiated, and we just learned that sealed classes cannot be extended outside of the file where they are defined.

Here is the new case class:

Scala code
abstract sealed case class EMail(value: String) {
  import java.net.URLEncoder
def link(asCode: Boolean=true): String = s"${ if (asCode) "<code>" else "" }<a href=’mailto:$value’>$value</a>${ if (asCode) "</code>" else "" }"
/** Generates a mailto: link with the optional subject and/or body. The subject and/or body will be URLEncoded. */ def mailTo(subject: String="", body: String=""): String = { import java.nio.charset.StandardCharsets.UTF_8 val queryString = if ((subject + body).trim.isEmpty) "" else "?" + (if (subject.trim.isEmpty) "" else "subject=" + URLEncoder.encode(subject.trim, UTF_8.toString)) + (if (subject.nonEmpty && body.nonEmpty) "&" else "") + (if (body.trim.isEmpty) "" else "body=" + URLEncoder.encode(body.trim, UTF_8.toString)) s"""mailto:${ link() }$queryString""" }
override def toString = value }

Here is the new companion object. Note that the Scala compiler does not generate apply methods in the companion object of abstract case classes, so we can define a method called apply, instead of defining a special factory method like fromString:

Scala code
object EMail {
  import java.util.regex.Pattern
  val emailRegex = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE)
/* Special marker value for invalid emails */ val empty = new EMail("x@y.com"){}
def apply(value: String) = if (emailRegex.matcher(value).find) new EMail(value) {} else empty }

Here is how to use the new EMail case class. First, let’s observe that the primary constructor cannot be used because the case class is abstract and sealed:

Shell
$ sbt console
... output suppressed ...
scala> import AbstractSealed2._
import AbstractSealed2._
scala> %}new EMail("blah@ick.com") <console>:20: error: constructor EMail in class EMail cannot be accessed in object $iw new EMail("blah@ick.com")

The apply factory method in the companion object is the only possible way to create an instance:

SBT REPL (continued)
scala> val email1 = EMail("santa@claus.com")
email1: AbstractSealed2.EMail = santa@claus.com 

Pattern-matching and unapply still work as usual.

SBT REPL (continued)
scala> EMail.unapply(email1)
res1: Option[String] = Some(santa@claus.com) 

The value field is public:

SBT REPL (continued)
scala> email1.value
res2: String = santa@claus.com 

equals does the right thing.

SBT REPL (continued)
scala> EMail("hi@bye.com") == EMail("hi@bye.com")
res13: Boolean = true 

The copy constructor is disabled, which is desirable because otherwise instances could be created that were not subject to the initialization logic.

SBT REPL (continued)
scala> email1.copy("blah@ick.com")
<console>:20: error: value copy is not a member of AbstractSealed2.EMail
       email1.copy("blah@ick.com")
                  ^ 

Aliases

Aliases allow you to match on type, as well as against property values if desired, and to obtain the entire object.

Aliases can be specified on patterns or on parts of a pattern. The alias is put before the pattern, separated by @.

For the alias address below, we define a filter that finds Addresses in Paris, France and returns the result as the alias address.

Scala REPL
case class Address(street: String, street2: String, city: String, country: String)
val addresses = List( Address("123 Main St", "Apt 3", "Yourtown", "MD"), Address("234 Rue Blue", "Apt 5", "Fontaineblue", "France"), Address("543 Toulouse", "Apt 6", "Paris", "France") )
addresses foreach { _ match { case address @ Address(_, _, "Paris", "France") => println(address.street) case _ => } }
Output
543 Toulouse

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.