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/
.
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.
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:
[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.
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> 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:
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:
$ 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.
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:
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.
$ 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:
-
Companion objects have factory methods called
apply
which are not masked when the primary constructor is markedprivate
.Scala REPLscala> val email2 = EMail("santa@claus.com") email2: AbstractSealed1.EMail = santa@claus.com
-
As we learned in the Case Classes lecture,
case classes have
copy
constructors.Scala REPLscala> 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:
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 class
es,
so we can define a method called apply
, instead of defining a special factory method like fromString
:
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
:
$ 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:
scala> val email1 = EMail("santa@claus.com") email1: AbstractSealed2.EMail = santa@claus.com
Pattern-matching and unapply
still work as usual.
scala> EMail.unapply(email1) res1: Option[String] = Some(santa@claus.com)
The value
field is public
:
scala> email1.value res2: String = santa@claus.com
equals
does the right thing.
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.
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 Address
es in Paris, France and returns the result as the alias address
.
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 _ => } }
543 Toulouse
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.