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
