Skip to content

Scala Extractors

Extractors are objects in Scala that define an unapply method. The primary purpose of an extractor is to deconstruct a value, usually within a pattern match (case clause), effectively reversing the construction process.

While often paired with an apply method (for construction), only the unapply method is required for an object to be an extractor.

The unapply method takes the value to be deconstructed as input and typically returns an Option of the extracted component(s).

  • For single value extraction: def unapply(input: InputType): Option[ExtractedType]
  • For multiple value extraction: def unapply(input: InputType): Option[(Type1, Type2, ...)] (returns an Option of a Tuple)

The Option return type handles cases where the input doesn’t match the expected structure: Some(...) indicates a successful match and extraction, while None indicates failure.

Let’s create an extractor object to parse email addresses into user and domain parts.

object Email {
// Optional: The 'apply' method for construction (Injection)
def apply(user: String, domain: String): String = {
s"$user@$domain"
}
// Mandatory: The 'unapply' method for deconstruction (Extraction)
def unapply(emailString: String): Option[(String, String)] = {
val parts = emailString.split('@')
if (parts.length == 2 && parts(0).nonEmpty && parts(1).nonEmpty) {
Some((parts(0), parts(1))) // Success: return Some(tuple)
} else {
None // Failure: return None
}
}
}
@main def ExtractorDemo(): Unit = {
// Using apply (optional)
val emailAddr = Email("alice", "example.com")
println(s"Constructed: $emailAddr")
// Using unapply implicitly via pattern matching
def checkEmail(addr: String): Unit = addr match {
case Email(user, domain) => // unapply is called here!
println(s"Matched! User: $user, Domain: $domain")
case _ =>
println(s"'$addr' is not a valid email address.")
}
checkEmail("bob@test.org")
checkEmail("invalid-email")
checkEmail(emailAddr) // Check the one we constructed
}

Output:

Constructed: alice@example.com
Matched! User: bob, Domain: test.org
'invalid-email' is not a valid email address.
Matched! User: alice, Domain: example.com

In the match statement, case Email(user, domain) => ... triggers the Email.unapply(addr) call. If it returns Some((u, d)), the match succeeds, and u is bound to user, d to domain. If it returns None, the case fails.

A key feature of case classes is that the Scala compiler automatically generates a companion object with apply and unapply methods based on the class parameters. This makes them work seamlessly with pattern matching out-of-the-box.

case class Person(name: String, age: Int)
val alice = Person("Alice", 30)
alice match {
// Compiler-generated Person.unapply(alice) is called
case Person(n, a) => println(s"Name: $n, Age: $a")
}

An unapply method can also return a Boolean for simple checks (though Option[Boolean] or custom types are often clearer). If unapply(value) returns true, the pattern matches.

object IsEven {
def unapply(num: Int): Boolean = num % 2 == 0
}
@main def BooleanExtractorDemo(): Unit = {
val number = 42
number match {
case IsEven() => println(s"$number is even.") // Matches if IsEven.unapply(number) is true
case _ => println(s"$number is odd.")
}
val oddNum = 7
oddNum match {
case IsEven() => println(s"$oddNum is even.")
case _ => println(s"$oddNum is odd.") // Matches here
}
}

Variable Argument Extractors (unapplySeq):

Section titled “Variable Argument Extractors (unapplySeq):”

For matching sequences of elements, an extractor can define unapplySeq. It returns an Option of a sequence type (like Seq[T]). This allows patterns like case MyExtractor(a, b, _*) => ....

object DomainParts {
// Extracts domain parts separated by dots
def unapplySeq(domain: String): Option[Seq[String]] = {
val parts = domain.split('.').toSeq
if (parts.nonEmpty && parts.forall(_.nonEmpty)) Some(parts) else None
}
}
@main def UnapplySeqDemo(): Unit = {
"www.example.com" match {
// Matches if 3 parts exist, ignores the rest
case DomainParts(first, second, third, _*) =>
println(s"Major parts: $first, $second, $third") // www, example, com
case _ => println("Could not parse domain")
}
"example.co.uk" match {
case DomainParts(name, tld1, tld2) => // Exact match for 3 parts
println(s"Name: $name, TLDs: $tld1.$tld2") // example, co.uk
case _ => println("Not a 3-part domain")
}
}

Extractors provide a powerful mechanism for custom pattern matching, enabling clean deconstruction of complex data types.