scalaintermediate

Custom Extractors with unapply

Create custom pattern matching extractors: unapply, unapplySeq, and boolean extractors.

scala
// Simple extractor
object Email:
  def unapply(s: String): Option[(String, String)] =
    s.split("@") match
      case Array(local, domain) if local.nonEmpty && domain.contains(".") =>
        Some((local, domain))
      case _ => None

// Boolean extractor (no value, just test)
object Even:
  def unapply(n: Int): Boolean = n % 2 == 0

object Positive:
  def unapply(n: Int): Boolean = n > 0

// Extracting multiple values
object RGB:
  def unapply(hex: String): Option[(Int, Int, Int)] =
    val clean = hex.stripPrefix("#")
    if clean.length == 6 then
      try
        val r = Integer.parseInt(clean.substring(0, 2), 16)
        val g = Integer.parseInt(clean.substring(2, 4), 16)
        val b = Integer.parseInt(clean.substring(4, 6), 16)
        Some((r, g, b))
      catch case _: NumberFormatException => None
    else None

// Sequence extractor
object Words:
  def unapplySeq(s: String): Option[Seq[String]] =
    val words = s.trim.split("\\s+").toSeq
    if words.nonEmpty && words.head.nonEmpty then Some(words)
    else None

// Name extractor
object Name:
  def unapply(fullName: String): Option[(String, Option[String], String)] =
    fullName.trim.split("\\s+").toList match
      case first :: last :: Nil => Some((first, None, last))
      case first :: middle :: last :: Nil => Some((first, Some(middle), last))
      case _ => None

// URL extractor
object URL:
  def unapply(url: String): Option[(String, String, String)] =
    val pattern = """(https?)://([^/]+)(/.*)?""".r
    url match
      case pattern(scheme, host, path) =>
        Some((scheme, host, Option(path).getOrElse("/")))
      case _ => None

// Range extractor
object Between:
  def unapply(n: Int): Option[(Int, Int, Int)] =
    Some((n, n, n))  // just passes through for guard use

// Infix extractor pattern
case class ~[+A, +B](a: A, b: B)

@main def run(): Unit =
  // Email
  "alice@example.com" match
    case Email(local, domain) => println(s"Email: $local @ $domain")
    case _ => println("Not an email")

  "not-email" match
    case Email(_, _) => println("Email")
    case s => println(s"Not email: $s")

  // Boolean extractors
  42 match
    case Even() => println("42 is even")
    case _      => println("42 is odd")

  // Combined
  val n = 7
  n match
    case Even() & Positive() => println(s"$n: even and positive")
    case Positive()          => println(s"$n: positive but odd")
    case _                   => println(s"$n: other")

  // RGB
  "#FF8000" match
    case RGB(r, g, b) => println(s"Color: R=$r, G=$g, B=$b")
    case _ => println("Invalid")

  // Words
  "hello beautiful world" match
    case Words(first, _*) => println(s"First word: $first")
    case _ => println("No words")

  "the quick brown fox" match
    case Words(a, b, c, d) => println(s"Four words: $a $b $c $d")
    case _ => ()

  // Name
  List("John Doe", "John M Doe", "Cher").foreach {
    case Name(first, Some(mid), last) =>
      println(s"  $first $mid. $last")
    case Name(first, None, last) =>
      println(s"  $first $last")
    case name =>
      println(s"  Cannot parse: $name")
  }

  // URL
  "https://example.com/api/v1" match
    case URL(scheme, host, path) =>
      println(s"URL: $scheme://$host$path")
    case _ => println("Invalid URL")

Use Cases

  • Domain-specific pattern matching
  • Parsing structured strings
  • Custom destructuring in match

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.