Scala Extractors
Scala Extractors
Section titled “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 Signature:
Section titled “The unapply Method Signature:”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.
Example: Email Extractor
Section titled “Example: Email Extractor”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.comMatched! User: bob, Domain: test.org'invalid-email' is not a valid email address.Matched! User: alice, Domain: example.comIn 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.
Case Classes and Extractors:
Section titled “Case Classes and Extractors:”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")}Extractors for Boolean Checks:
Section titled “Extractors for Boolean Checks:”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.