Mike Slinn

Unapply

— Draft —

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/src/main/scala/Extractors.scala.

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 REPL
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 REPL
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 an Option[T].
  • If you want to return several properties T1,...,Tn, group them in an Option of the required tuple Option[(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:

  1. String has an indexOf(search: String) method that could be used to detect the space between the first and last name
  2. String has a split(delim: String) method that could be used to create an Array[String] containing tokens from the original String
  3. 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 REPL
    scala> Some(("first", "last"))
    res9: Some[(String, String)] = Some((first,last)) 

Here is the class and its companion object, which you need to complete:

Scala code
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.

Scala code
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/src/main/scala/solutions/Unapply.scala.

Run them like this:

Shell
$ sbt ~"runMain solutions.Unapply"

... and:

Shell
$ 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.

Scala code
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 calling any.toBoolean. For example, "true".toBoolean.
  • Similarly, you can convert an object to its Int equivalent by calling toInt. For example, "123".toInt.

Solution

Scala code
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/src/main/scala/solutions/Unapply3.scala.

You can run it like this:

Shell
$ 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 REPL
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 REPL
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 REPL
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 Functions. The following works since Scala 2.11:

Scala REPL
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.

Scala code
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:

Scala code
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:

Scala code
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.

Shell
$ 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

  1. To read arguments from the command line in a console app, reference the args variable. You can discover the number of variables by reading args.size, and you can obtain the first argument as args(0).
  2. Do not worry about error handling.

Solution

Scala code
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.

Shell
$ sbt "runMain solutions.Extractor true 4 false"

* 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.