Published 2014-01-11.
Last modified 2014-07-18.
Time to read: 4 minutes.
Extractors are one of Scala's most powerful features. The unapply
method makes extraction work.
Scala 3 changed the behavior and the signature of the unapply
method for case classes.
This lecture discusses the Scala 2 and Scala 3 differences.
The sample code for this lecture can be found in
courseNotes/
.
Unapply
Unapply
is a method that can be defined in a companion object.
If it exists it will automatically be called when the Scala compiler needs to extract values from a Scala class or case class.
Unapply
is therefore useful for pattern matching.
The value returned by unapply
is often an Option
of a tuple.
Let’s add an unapply
method to the Frog6
class and save this new class as Frog8
.
The unapply
method will merely accept an instance of the companion class and return an Option
of a tuple containing the primary constructor properties.
In order for this to work, all parameters provided to the primary constructor must be public properties.
Recall that for regular classes this means that each primary constructor parameter must be prefaced with the
val
or var
keywords.
scala> :paste // Entering paste mode (ctrl-D to finish)
scala> class Frog8(val canSwim: Boolean, val numLegs: Int, val breathesAir: Boolean) { override def toString() = s"canSwim: $canSwim; $numLegs legs; breathesAir: $breathesAir" }
object Frog8 { def apply(canSwim: Boolean=true, numLegs: Int=4, breathesAir: Boolean=true) = new Frog8(canSwim, numLegs, breathesAir)
def unapply(frog: Frog8):Option[(Boolean, Int, Boolean)] = Some((frog.canSwim, frog.numLegs, frog.breathesAir)) } ^D // Exiting paste mode, now interpreting.
defined class Frog8 defined module Frog8
The unapply
method above returns an Option[Tuple3]
, expressed as doublely nested parentheses:
Some((frog.canSwim, frog.numLegs, frog.breathesAir))
.
More precisely, this unapply
method returns an Option[Tuple3[Boolean, Int, Boolean]]
.
You may see this written sometimes with only one parenthesis:
Some(frog.canSwim, frog.numLegs, frog.breathesAir)
This is possible because when the Scala compiler expects to see an Option[TupleN[...]]
but instead only finds TupleN[...]
it will automatically wrap the TupleN
in an Option
.
If compiler verbosity is set to WARN
, then a message will be displayed letting you know that your code was modified.
I suggest you write two nested parentheses, because that is what you actually mean.
scala> val frog8 = Frog8(canSwim=true, 4, breathesAir=false) frog8: Frog8 = canSwim: true; 4 legs; breathesAir: false
scala> // implicitly calls Frog8.unapply: scala> val Frog8(a1, b1, c1) = frog8 a1: Boolean = true b1: Int = 4 c1: Boolean = false
scala> // implicitly calls Frog8.unapply: scala> val Frog8(a2, b2, c2) = Frog8(canSwim=false, 2, breathesAir=false) a2: Boolean = false b2: Int = 2 c2: Boolean = false
unapply Return Types
Define the return type of an unapply
method as follows:
- If it returns a single property of type
T
, return anOption[T]
. -
If you want to return several properties
T1,...,Tn
, group them in anOption
of the required tupleOption[(T1, ..., Tn)]
.
Exercise
The following class definition has an incomplete unapply
method in the companion object.
Can you write a Scala program that parses the input String
such that an instance of Some[Tuple2[String, String]]
containing the first and last name is returned in the event of a successful parse, or None
is returned if the parse fails?
Hints:
-
String
has anindexOf(search: String)
method that could be used to detect the space between the first and last name -
String
has asplit(delim: String)
method that could be used to create anArray[String]
containing tokens from the originalString
-
You can create an instance of
Some[Tuple2]
this way:Some(("first", "last"))
. If you try this in the REPL you should see the following:Scala REPLscala> Some(("first", "last")) res9: Some[(String, String)] = Some((first,last))
Here is the class and its companion object, which you need to complete:
class Name(val first: String, val last: String)
object Name { def unapply(input:String) = { // TODO write me! } }
val Name(firstName, lastName) = "Fred Flintstone"
When the Scala interpreter encounters the last line above,
it looks for an unapply
method in Name
’s companion object with the proper signature.
In this case, it looks for an unapply
method that accepts a String
.
As a final hint, the REPL needs to receive a class and its companion object simultaneously in order to consider them as residing in the same file.
Use the REPL’s :paste
command to accomplish this.
Solution
Here are several solutions.
package solutions
object Unapply extends App { class Name(val first: String, val last: String)
object Name { def unapply(input:String): Option[(String, String)] = { val stringArray = input.split(" ") if (stringArray.length>=2) Some(stringArray(0), stringArray(1)) else None } }
val Name(firstName, lastName) = "Fred Flintstone" println(firstName + " " + lastName) }
object Unapply2 extends App { class Name(val first: String, val last: String)
object Name { def apply(a: String, b: String): Name = new Name(a, b)
def unapply(name: Name): Option[(String, String)] = Some((name.first, name.last))
def unapply(input: String): Option[(String, String)] = { val stringArray = input.split(" ") if (stringArray.length>=2) Some(stringArray(0), stringArray(1)) else None } }
val name: Name = try { val Name(firstName, lastName) = "Fred Flintstone" println(s"$firstName $lastName") Name(firstName, lastName) } catch { case me: MatchError => println(s"Failed parse: ${me.getMessage()}") Name("nobody", "atAll") case huh: Throwable => println(huh.getMessage) Name("Uno", "Who") } finally { Name("Wilma", "Flintstone") } println(s"Matched type ${name.getClass.getName}")
name match { case Name(fName, _) if fName != "Fred" => println(s"Got one: $fName")
case Name(fName, "Flintstone") => println(fName)
case name: Name => println(name.first) } }
These solutions are provided in
courseNotes/
.
Run them like this:
$ sbt ~"runMain solutions.Unapply"
... and:
$ sbt ~"runMain solutions.Unapply2"
Overloading unapply Again
When writing regular classes (not case classes) you should always define an unapply
method in the companion object that accepts one
parameter, and the type of that parameter should be the type of the companion class.
Case classes do this for you automatically.
You can also overload unapply
by defining methods called unapply
which accept parameters of any type that you wish to
parse into instances of the companion class.
For example, if you wish to create instances of a Frog8
from information contained in a String
,
the method signature you need to implement is.
object Frog8 { def unapply(string: String): Option[(Boolean, Int, Boolean)] = ??? }
Exercise
Implement the above unapply
such that it can parse the following String
: "true 4 false"
.
Don’t worry about error handling.
Hints
-
A companion object and companion class must be defined in the same scope.
Be sure to create a file that contains a copy of the
Frog8
source code. -
You can split a string into an array of space-delimited tokens by calling
"my string".split(" ")
. -
You can access the nth item in an array by using parentheses to supply the index, like this:
myArray(2)
. -
You can convert an object to its
Boolean
equivalent by callingany.toBoolean
. For example,"true".toBoolean
. -
Similarly, you can convert an object to its Int equivalent by calling
toInt
. For example,"123".toInt
.
Solution
object Unapply3 { class Frog8(val canSwim: Boolean, val numLegs: Int, val breathesAir: Boolean) { override def toString = s"canSwim: $canSwim; $numLegs legs; breathesAir: $breathesAir" }
object Frog8 { def apply(canSwim: Boolean=true, numLegs: Int=4, breathesAir: Boolean=true) = new Frog8(canSwim, numLegs, breathesAir)
def unapply(frog: Frog8):Option[(Boolean, Int, Boolean)] = Some((frog.canSwim, frog.numLegs, frog.breathesAir))
def unapply(string: String): Option[(Boolean, Int, Boolean)] = { val tokens = string.split(" ") if (tokens.length>=3) Some((tokens(0).toBoolean, tokens(1).toInt, tokens(2).toBoolean)) else None } } }
This solution is provided in
courseNotes/
.
You can run it like this:
$ sbt "runMain solutions.Unapply3"
Partial match
If you only want to retrieve some of the values and not others, use an underscore in place of the properties that you are not interested in extracting.
scala> val Frog8(a3, _, c3) = frog8 // implicitly calls Frog8.unapply a3: Boolean = true c3: Boolean = false
While an underscore is used as a wildcard for much of Scala, in this context an underscore means that parsed value need not be stored.
Case Classes
Case classes automatically define apply
and unapply
.
scala> case class Dog(name: String, barksTooMuch: Boolean) defined class Dog
scala> val bigDog = Dog("Fido", barksTooMuch=false) bigDog: Dog = Dog(Fido,false)
scala> val Dog(x, y) = bigDog // implicitly calls Dog.unapply x: String = Fido y: Boolean = false
Notice that two values were returned by the computation and stored into x
and y
.
Unapply
deconstructed the Dog
returned by the computation and extracted the two parameters.
Overloading unapply
You can define overloaded versions of apply
and unapply
by augmenting the definition of a case class’s automatically
generated companion object.
Lets do this for a case class called Fraction
which models fractions.
scala> :paste // Entering paste mode (ctrl-D to finish)
scala> case class Fraction(var numerator:Int, var denominator:Int) { def *(fraction: Fraction) = Fraction(numerator*fraction.numerator, denominator*fraction.denominator)
override def toString = s"$numerator/$denominator" }
scala> object Fraction { // this augments the automatically generated companion object instead of replacing it def unapply(string: String): Option[(Int, Int)] = { val tokens = string.split("/") if (tokens.length!=2) None else Some(tokens(0).toInt, tokens(1).toInt) } } ^D
// Exiting paste mode, now interpreting.
defined class Fraction defined module Fraction
scala> val fraction = Fraction(3,4) * Fraction(2,4) fraction: String = 6/16
scala> val Fraction(numer, denom) = "3/4"// implicitly calls Fraction.unapply numer: Int = 3 denom: Int = 4
Again, notice that two values were returned by the computation and stored into numer
and denom
.
Unapply
parsed the String
"3/4"
and returned an Option[Tuple2[Int, Int]]
containing
the two parsed parameters.
Arity 22 Limitation Partially Removed
Scala 2.11 improved on the arity 22 limitation of case classes,
but did not remove the arity 22 limitation of tuples or Function
s.
The following works since Scala 2.11:
scala> type A = Int // defined alias type A = Int
scala> case class Foo(a: A, b: A, c: A, d: A, e: A, f: A, g: A, h: A, i: A, j: A, k: A, l: A, m: A, n: A, o: A, p: A, q: A, r: A, s: A, t: A, u: A, v: A, w: A, x: A, y: A, Z: A) // defined case class Foo
scala> val foo = Foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6) val foo: Foo = Foo(1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6)
scala> val Foo(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z) = foo val a: A = 1 val b: A = 2 val c: A = 3 val d: A = 4 val e: A = 5 val f: A = 6 val g: A = 7 val h: A = 8 val i: A = 9 val j: A = 0 val k: A = 1 val l: A = 2 val m: A = 3 val n: A = 4 val o: A = 5 val p: A = 6 val q: A = 7 val r: A = 8 val s: A = 9 val t: A = 0 val u: A = 1 val v: A = 2 val w: A = 3 val x: A = 4 val y: A = 5 val z: A = 6
The last line above succeeded because Scala now allows more than 22 arguments for unapply
.
Exercise
You can augment a case class’s companion object simply by defining new methods and overloading existing methods.
Your task is to write a Scala program that defines another unapply
method for Frog9
’s companion object which accepts
a String
(or an Array[String]
) and parses it into a Option[Tuple3]
, which means a successful parse will
return Some((canSwim, numLegs, breathesAir))
or None
if the parse fails.
object Frog9 { def unapply(input: String): Option[(Boolean, Int, Boolean)] = ??? // TODO write me def unapply(input: Array[String]): Option[(Boolean, Int, Boolean)] = ??? // TODO write me }
case class Frog9(canSwim: Boolean, numLegs: Int, breathesAir: Boolean)
Test your code like this:
val Frog9(swimmer1, legCount1, airBreather1) = "true 4 true" val Frog9(swimmer2, legCount2, airBreather2) = "true 0 false"
...or if you opted for the second version of unapply:
val Frog9(swimmer3, legCount3, airBreather3) = Array("true", "4", "true") val Frog9(swimmer4, legCount4, airBreather4) = Array("true", "0", "false")
The program should read the arguments from the command line.
If you put your program in courseNotes/src/main/scala
and call it Extractor.scala
then you should be able to run it and
display the parsed values like this.
$ sbt ~"runMain Extractor true 4 true" swimmer = true legCount = 4 airBreather = true
$ sbt "runMain Extractor true 0 false" swimmer = true legCount = 0 airBreather = false
Hints
-
To read arguments from the command line in a console app, reference the
args
variable. You can discover the number of variables by readingargs.size
, and you can obtain the first argument asargs(0)
. - Do not worry about error handling.
Solution
package solutions
object Extractor extends App { type FrogTuple = (Boolean, Int, Boolean) // same as Tuple3[Boolean, Int, Boolean] type MaybeFrogTuple = Option[FrogTuple] // same as Option[Tuple3[Boolean, Int, Boolean]]
case class Frog9(canSwim: Boolean, numLegs: Int, breathesAir: Boolean)
object Frog9 { def unapply(input: String): MaybeFrogTuple = parse(input.split(" "))
def unapply(input: Array[String]): MaybeFrogTuple = parse(input)
private def parse(input: Array[String]): MaybeFrogTuple = if (input.size!=3) None else try { Some((args(0).toBoolean, args(1).toInt, args(2).toBoolean)) } catch { case e: Exception => None } }
def test(): Unit = { val Frog9(swimmer1, legCount1, airBreather1) = "true 4 true" val Frog9(swimmer2, legCount2, airBreather2) = "true 0 false" val Frog9(swimmer3, legCount3, airBreather3) = Array("true", "4", "true") val Frog9(swimmer4, legCount4, airBreather4) = Array("true", "0", "false") }
// test()
val Frog9(swimmer, legCount, airBreather) = args println(s"swimmer = $swimmer") println(s"legCount = $legCount") println(s"airBreather = $airBreather") }
This solution is provided in courseNotes/src/main/scala/solutions/Extractor.scala
.
You can run it like this.
$ sbt "runMain solutions.Extractor true 4 false"
© 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.