Scala Extractors

An extractor is an object that defines an unapply method. When you use a pattern in a match expression, Scala calls unapply behind the scenes to break the value into parts. Case classes provide unapply automatically, but you can write custom extractors for any class or transformation.

How unapply Works


match expression:    case Person(name, age) => ...

Scala calls:         Person.unapply(value)
Returns:             Some((name, age))  →  match succeeds, name/age bound
                     None              →  match fails, try next case

Custom Extractor

class Email(val address: String)

object Email:
  def apply(address: String): Email = new Email(address)

  def unapply(email: Email): Option[String] =
    Some(email.address)   // extract the address

val e = Email("alice@example.com")

e match
  case Email(addr) => println(s"Email address: $addr")
// Email address: alice@example.com

Extractor That Deconstructs Into Parts

class IPAddress(val address: String)

object IPAddress:
  def unapply(ip: IPAddress): Option[(Int, Int, Int, Int)] =
    val parts = ip.address.split("\\.")
    if parts.length == 4 then
      try
        Some((parts(0).toInt, parts(1).toInt, parts(2).toInt, parts(3).toInt))
      catch case _: Exception => None
    else None

val ip = new IPAddress("192.168.1.100")

ip match
  case IPAddress(192, 168, subnet, host) =>
    println(s"Private LAN: subnet=$subnet, host=$host")
  case IPAddress(127, 0, 0, 1) =>
    println("Localhost")
  case IPAddress(a, b, c, d) =>
    println(s"Other IP: $a.$b.$c.$d")

// Private LAN: subnet=1, host=100

Boolean Extractor (unapply Returns Boolean)

object EvenNumber:
  def unapply(n: Int): Boolean = n % 2 == 0

object LargeNumber:
  def unapply(n: Int): Boolean = n > 100

val numbers = List(2, 55, 102, 7, 200, 13)

numbers.foreach {
  case EvenNumber() if LargeNumber() => println(s"Large even")
  case EvenNumber()                  => println(s"Small even")
  case LargeNumber()                 => println(s"Large odd")
  case n                             => println(s"Small odd: $n")
}
// Small even
// Large odd
// Large even
// Small odd: 7
// Large even
// Small odd: 13

String Pattern Extractors

object FullName:
  def unapply(s: String): Option[(String, String)] =
    s.trim.split("\\s+", 2) match
      case Array(first, last) => Some((first, last))
      case _                  => None

object Email:
  def unapply(s: String): Option[(String, String)] =
    s.split("@", 2) match
      case Array(user, domain) => Some((user, domain))
      case _                   => None

val inputs = List("Alice Johnson", "bob@example.com", "Carol Smith", "notvalid")

inputs.foreach {
  case Email(user, domain) => println(s"Email: user=$user, domain=$domain")
  case FullName(f, l)      => println(s"Name: $f $l")
  case other               => println(s"Unknown: $other")
}
// Name: Alice Johnson
// Email: user=bob, domain=example.com
// Name: Carol Smith
// Unknown: notvalid

unapplySeq — Variable-length Patterns

Use unapplySeq when the number of extracted values is not fixed — like matching a list of words:

object Words:
  def unapplySeq(s: String): Option[Seq[String]] =
    Some(s.trim.split("\\s+").toSeq)

"hello world scala" match
  case Words(a, b, c)    => println(s"Three words: $a, $b, $c")
  case Words(a, b)       => println(s"Two words: $a, $b")
  case Words(a)          => println(s"One word: $a")
  case Words(a, b, rest @ _*) => println(s"Many: $a, $b, ...")

// Three words: hello, world, scala

Extractor with Regex

val DatePattern = """(\d{4})-(\d{2})-(\d{2})""".r

def parseDate(s: String): String = s match
  case DatePattern(year, month, day) =>
    s"Year: $year, Month: $month, Day: $day"
  case _ =>
    s"Not a valid date: $s"

println(parseDate("2024-07-15"))   // Year: 2024, Month: 07, Day: 15
println(parseDate("not-a-date"))   // Not a valid date: not-a-date

Extractors vs Case Classes


Case Class Extractor              Custom Extractor
────────────────────────────────  ────────────────────────────────
Auto-generated by compiler        You write unapply manually
For your own classes              For any value, any type
Matches exact field structure     Can transform and validate
Simple to use                     Flexible — any extraction logic

Practical: HTTP Request Extractor

case class Request(method: String, path: String, body: Option[String])

object GET:
  def unapply(r: Request): Option[String] =
    if r.method == "GET" then Some(r.path) else None

object POST:
  def unapply(r: Request): Option[(String, String)] =
    if r.method == "POST" then
      r.body.map(b => (r.path, b))
    else None

def handle(req: Request): String = req match
  case GET("/")           => "Home page"
  case GET(path)          => s"GET $path"
  case POST("/login", body) => s"Login with: $body"
  case POST(path, _)      => s"POST to $path"
  case _                  => "Unknown request"

println(handle(Request("GET",  "/",       None)))
println(handle(Request("GET",  "/about",  None)))
println(handle(Request("POST", "/login",  Some("user=alice&pass=secret"))))
// Home page
// GET /about
// Login with: user=alice&pass=secret

Leave a Comment

Your email address will not be published. Required fields are marked *