#Scala Extractors
Extractors are a powerful feature in Scala that allows you to define how to extract values from objects. Extractors are implemented through the unapply method and are the foundation of pattern matching.
#Basic Extractors
#unapply Method
object BasicExtractors {
// Simple extractor
object Even {
def unapply(n: Int): Option[Int] = {
if (n % 2 == 0) Some(n) else None
}
}
object Odd {
def unapply(n: Int): Option[Int] = {
if (n % 2 != 0) Some(n) else None
}
}
// Extractors can return different values
object Square {
def unapply(n: Int): Option[Int] = {
val sqrt = math.sqrt(n).toInt
if (sqrt * sqrt == n) Some(sqrt) else None
}
}
// Boolean extractors
object Positive {
def unapply(n: Int): Boolean = n > 0
}
object Negative {
def unapply(n: Int): Boolean = n < 0
}
def analyzeNumber(n: Int): String = n match {
case Even(x) => s"$x is even"
case Odd(x) => s"$x is odd"
}
def analyzeSpecialNumbers(n: Int): String = n match {
case Square(root) => s"$n is a perfect square, square root is $root"
case Positive() => s"$n is positive"
case Negative() => s"$n is negative"
case _ => s"$n is zero"
}
def main(args: Array[String]): Unit = {
val numbers = List(1, 2, 3, 4, 9, 16, -5, 0, 25)
println("Number analysis:")
numbers.foreach(n => println(s"$n -> ${analyzeNumber(n)}"))
println("\nSpecial number analysis:")
numbers.foreach(n => println(s"$n -> ${analyzeSpecialNumbers(n)}"))
}
}#Multi-value Extractors
object MultiValueExtractors {
// Extractors that extract multiple values
object FullName {
def unapply(fullName: String): Option[(String, String)] = {
val parts = fullName.split(" ")
if (parts.length == 2) Some((parts(0), parts(1)))
else None
}
}
// Extractors that extract variable number of values
object Words {
def unapplySeq(sentence: String): Option[Seq[String]] = {
val words = sentence.split("\\s+").filter(_.nonEmpty)
if (words.nonEmpty) Some(words.toSeq) else None
}
}
// Number range extractor
object Range {
def unapply(input: String): Option[(Int, Int)] = {
val rangePattern = """(\d+)-(\d+)""".r
input match {
case rangePattern(start, end) => Some((start.toInt, end.toInt))
case _ => None
}
}
}
// Coordinate extractor
object Coordinate {
def unapply(input: String): Option[(Double, Double)] = {
val coordPattern = """\((-?\d+\.?\d*),\s*(-?\d+\.?\d*)\)""".r
input match {
case coordPattern(x, y) => Some((x.toDouble, y.toDouble))
case _ => None
}
}
}
def processInput(input: String): String = input match {
case FullName(first, last) =>
s"Name: $first $last"
case Words(first, second, rest @ _*) =>
s"Sentence: First word='$first', Second word='$second', Rest=${rest.mkString(", ")}"
case Range(start, end) =>
s"Range: From $start to $end"
case Coordinate(x, y) =>
s"Coordinate: ($x, $y)"
case _ =>
s"Unrecognized format: $input"
}
def main(args: Array[String]): Unit = {
val inputs = List(
"John Doe",
"The quick brown fox jumps",
"1-100",
"(3.14, 2.71)",
"single",
"invalid format"
)
inputs.foreach(input => println(s"'$input' -> ${processInput(input)}"))
}
}#Advanced Extractor Patterns
#Nested Extractors
object NestedExtractors {
case class Address(street: String, city: String, zipCode: String)
case class Person(name: String, age: Int, address: Address)
// Age group extractor
object AgeGroup {
def unapply(age: Int): Option[String] = age match {
case a if a < 18 => Some("Minor")
case a if a < 65 => Some("Adult")
case _ => Some("Senior")
}
}
// City type extractor
object CityType {
val majorCities = Set("Beijing", "Shanghai", "Guangzhou", "Shenzhen")
def unapply(city: String): Option[String] = {
if (majorCities.contains(city)) Some("First-tier city")
else Some("Other city")
}
}
// Zip code region extractor
object ZipRegion {
def unapply(zipCode: String): Option[String] = {
zipCode.take(2) match {
case "10" | "11" => Some("North China")
case "20" | "21" => Some("East China")
case "30" | "31" => Some("South China")
case _ => Some("Other")
}
}
}
def analyzePerson(person: Person): String = person match {
// Nested pattern matching
case Person(name, AgeGroup(ageGroup), Address(_, CityType(cityType), ZipRegion(region))) =>
s"$name is $ageGroup, lives in $cityType ($region region)"
case Person(name, age, Address(street, city, _)) if age > 60 =>
s"$name is a senior, lives on $street in $city"
case Person(name, _, Address(_, "Beijing", _)) =>
s"$name lives in the capital Beijing"
case Person(name, age, _) =>
s"$name, $age years old"
}
def main(args: Array[String]): Unit = {
val people = List(
Person("Zhang San", 25, Address("Zhongguancun Street", "Beijing", "100080")),
Person("Li Si", 16, Address("Nanjing Road", "Shanghai", "200000")),
Person("Wang Wu", 70, Address("Tianhe Road", "Guangzhou", "310000")),
Person("Zhao Liu", 35, Address("Jiefang Road", "Wuhan", "430000"))
)
people.foreach(person => println(analyzePerson(person)))
}
}#Conditional Extractors
object ConditionalExtractors {
// Extractor with conditions
object ValidEmail {
def unapply(email: String): Option[String] = {
if (email.contains("@") && email.contains(".")) Some(email.toLowerCase)
else None
}
}
object StrongPassword {
def unapply(password: String): Option[String] = {
val hasUpper = password.exists(_.isUpper)
val hasLower = password.exists(_.isLower)
val hasDigit = password.exists(_.isDigit)
val hasSpecial = password.exists("!@#$%^&*".contains(_))
val isLongEnough = password.length >= 8
if (hasUpper && hasLower && hasDigit && hasSpecial && isLongEnough) {
Some(password)
} else None
}
}
// Numeric range extractor
object InRange {
def unapply(value: Int): Option[String] = value match {
case v if v >= 0 && v <= 100 => Some("Normal range")
case v if v > 100 => Some("Exceeds upper limit")
case _ => Some("Below lower limit")
}
}
// File type extractor
object FileType {
def unapply(filename: String): Option[String] = {
val extension = filename.split("\\.").lastOption.map(_.toLowerCase)
extension match {
case Some("jpg" | "jpeg" | "png" | "gif") => Some("Image")
case Some("mp4" | "avi" | "mov") => Some("Video")
case Some("txt" | "doc" | "pdf") => Some("Document")
case Some("mp3" | "wav" | "flac") => Some("Audio")
case _ => Some("Other")
}
}
}
def validateUser(email: String, password: String): String = (email, password) match {
case (ValidEmail(validEmail), StrongPassword(strongPass)) =>
s"User validation successful: $validEmail"
case (ValidEmail(_), _) =>
"Email is valid, but password is not strong enough"
case (_, StrongPassword(_)) =>
"Password strength is sufficient, but email is invalid"
case _ =>
"Both email and password do not meet requirements"
}
def analyzeValue(value: Int): String = value match {
case InRange(status) => s"Value $value: $status"
}
def classifyFile(filename: String): String = filename match {
case FileType(fileType) => s"File '$filename' is $fileType type"
}
def main(args: Array[String]): Unit = {
// User validation tests
val userTests = List(
("user@example.com", "StrongPass123!"),
("invalid-email", "StrongPass123!"),
("user@example.com", "weak"),
("invalid", "weak")
)
println("User validation tests:")
userTests.foreach { case (email, password) =>
println(s"$email, $password -> ${validateUser(email, password)}")
}
// Numeric range tests
println("\nNumeric range tests:")
List(-10, 50, 150).foreach(value => println(analyzeValue(value)))
// File type tests
println("\nFile type tests:")
List("photo.jpg", "video.mp4", "document.pdf", "music.mp3", "data.csv")
.foreach(filename => println(classifyFile(filename)))
}
}#Custom Data Structure Extractors
#Linked List Extractors
object CustomDataStructureExtractors {
// Custom linked list
sealed trait MyList[+T]
case object MyNil extends MyList[Nothing]
case class MyCons[T](head: T, tail: MyList[T]) extends MyList[T]
// Linked list extractor
object MyList {
def apply[T](elements: T*): MyList[T] = {
elements.foldRight(MyNil: MyList[T])(MyCons(_, _))
}
// Extract head and tail
def unapply[T](list: MyList[T]): Option[(T, MyList[T])] = list match {
case MyCons(head, tail) => Some((head, tail))
case MyNil => None
}
}
// Special pattern extractors
object SingleElement {
def unapply[T](list: MyList[T]): Option[T] = list match {
case MyCons(head, MyNil) => Some(head)
case _ => None
}
}
object FirstTwo {
def unapply[T](list: MyList[T]): Option[(T, T)] = list match {
case MyCons(first, MyCons(second, _)) => Some((first, second))
case _ => None
}
}
// Binary tree
sealed trait BinaryTree[+T]
case object Empty extends BinaryTree[Nothing]
case class Node[T](value: T, left: BinaryTree[T], right: BinaryTree[T]) extends BinaryTree[T]
// Binary tree extractors
object Leaf {
def unapply[T](tree: BinaryTree[T]): Option[T] = tree match {
case Node(value, Empty, Empty) => Some(value)
case _ => None
}
}
object LeftChild {
def unapply[T](tree: BinaryTree[T]): Option[(T, BinaryTree[T])] = tree match {
case Node(value, left, Empty) => Some((value, left))
case _ => None
}
}
object RightChild {
def unapply[T](tree: BinaryTree[T]): Option[(T, BinaryTree[T])] = tree match {
case Node(value, Empty, right) => Some((value, right))
case _ => None
}
}
def analyzeList[T](list: MyList[T]): String = list match {
case MyNil => "Empty list"
case SingleElement(element) => s"Single element list: $element"
case FirstTwo(first, second) => s"First two elements: $first, $second"
case MyList(head, tail) => s"Head element: $head, Tail: ${analyzeList(tail)}"
}
def analyzeTree[T](tree: BinaryTree[T]): String = tree match {
case Empty => "Empty tree"
case Leaf(value) => s"Leaf node: $value"
case LeftChild(value, left) => s"Node with only left subtree: $value, left subtree: ${analyzeTree(left)}"
case RightChild(value, right) => s"Node with only right subtree: $value, right subtree: ${analyzeTree(right)}"
case Node(value, left, right) => s"Full node: $value, left: ${analyzeTree(left)}, right: ${analyzeTree(right)}"
}
def main(args: Array[String]): Unit = {
// Test linked list
val lists = List(
MyNil,
MyList(1),
MyList(1, 2),
MyList(1, 2, 3, 4)
)
println("Linked list analysis:")
lists.foreach(list => println(analyzeList(list)))
// Test binary tree
val trees = List(
Empty,
Node(1, Empty, Empty), // Leaf
Node(1, Node(2, Empty, Empty), Empty), // Only left subtree
Node(1, Empty, Node(3, Empty, Empty)), // Only right subtree
Node(1, Node(2, Empty, Empty), Node(3, Empty, Empty)) // Full tree
)
println("\nBinary tree analysis:")
trees.foreach(tree => println(analyzeTree(tree)))
}
}#Practical Application Examples
#URL Parser
object URLParser {
case class URL(protocol: String, host: String, port: Option[Int], path: String, query: Map[String, String])
object URL {
def unapply(urlString: String): Option[URL] = {
val urlPattern = """^(https?):\/\/([^:\/\s]+)(?::(\d+))?([^?\s]*)(?:\?(.*))?$""".r
urlString match {
case urlPattern(protocol, host, portStr, path, queryStr) =>
val port = Option(portStr).map(_.toInt)
val query = parseQuery(Option(queryStr).getOrElse(""))
Some(URL(protocol, host, port, if (path.isEmpty) "/" else path, query))
case _ => None
}
}
private def parseQuery(queryString: String): Map[String, String] = {
if (queryString.isEmpty) Map.empty
else {
queryString.split("&").map { param =>
val parts = param.split("=", 2)
parts(0) -> (if (parts.length > 1) parts(1) else "")
}.toMap
}
}
}
// Specific protocol extractors
object HttpsURL {
def unapply(url: URL): Option[URL] = {
if (url.protocol == "https") Some(url) else None
}
}
object LocalURL {
def unapply(url: URL): Option[URL] = {
if (url.host == "localhost" || url.host == "127.0.0.1") Some(url) else None
}
}
def analyzeURL(urlString: String): String = urlString match {
case URL(HttpsURL(url)) =>
s"Secure HTTPS connection: ${url.host}${url.path}"
case URL(LocalURL(url)) =>
s"Local connection: ${url.protocol}://${url.host}:${url.port.getOrElse("default port")}"
case URL(url) =>
s"Regular URL: ${url.protocol}://${url.host}${url.path}" +
(if (url.query.nonEmpty) s", query parameters: ${url.query}" else "")
case _ =>
s"Invalid URL: $urlString"
}
def main(args: Array[String]): Unit = {
val urls = List(
"https://www.example.com/path?param=value",
"http://localhost:8080/api/users",
"https://api.github.com/repos/owner/repo",
"http://127.0.0.1:3000/",
"invalid-url"
)
urls.foreach(url => println(s"$url -> ${analyzeURL(url)}"))
}
}#Log Parser
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
object LogParser {
case class LogEntry(
timestamp: LocalDateTime,
level: String,
logger: String,
message: String,
thread: Option[String] = None
)
// Standard log format extractor
object StandardLog {
private val pattern = """(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (\w+): (.+)""".r
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
def unapply(logLine: String): Option[LogEntry] = logLine match {
case pattern(timestampStr, level, logger, message) =>
try {
val timestamp = LocalDateTime.parse(timestampStr, formatter)
Some(LogEntry(timestamp, level, logger, message))
} catch {
case _: Exception => None
}
case _ => None
}
}
// Log format with thread info
object ThreadedLog {
private val pattern = """(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] \[([^\]]+)\] (\w+): (.+)""".r
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
def unapply(logLine: String): Option[LogEntry] = logLine match {
case pattern(timestampStr, level, thread, logger, message) =>
try {
val timestamp = LocalDateTime.parse(timestampStr, formatter)
Some(LogEntry(timestamp, level, logger, message, Some(thread)))
} catch {
case _: Exception => None
}
case _ => None
}
}
// Error level extractor
object ErrorLog {
def unapply(entry: LogEntry): Option[LogEntry] = {
if (entry.level == "ERROR") Some(entry) else None
}
}
object WarningLog {
def unapply(entry: LogEntry): Option[LogEntry] = {
if (entry.level == "WARN") Some(entry) else None
}
}
// Specific time range extractor
object RecentLog {
def unapply(entry: LogEntry): Option[LogEntry] = {
val now = LocalDateTime.now()
val oneHourAgo = now.minusHours(1)
if (entry.timestamp.isAfter(oneHourAgo)) Some(entry) else None
}
}
def analyzeLogEntry(logLine: String): String = logLine match {
case ThreadedLog(ErrorLog(entry)) =>
s"🔴 Thread error: [${entry.thread.get}] ${entry.logger} - ${entry.message}"
case StandardLog(ErrorLog(entry)) =>
s"🔴 Error: ${entry.logger} - ${entry.message}"
case ThreadedLog(WarningLog(entry)) =>
s"🟡 Thread warning: [${entry.thread.get}] ${entry.logger} - ${entry.message}"
case StandardLog(WarningLog(entry)) =>
s"🟡 Warning: ${entry.logger} - ${entry.message}"
case ThreadedLog(RecentLog(entry)) =>
s"🕐 Recent thread log: [${entry.thread.get}] ${entry.level} - ${entry.message}"
case StandardLog(RecentLog(entry)) =>
s"🕐 Recent log: ${entry.level} - ${entry.message}"
case ThreadedLog(entry) =>
s"📝 Thread log: [${entry.thread.get}] ${entry.level} - ${entry.logger}"
case StandardLog(entry) =>
s"📝 Standard log: ${entry.level} - ${entry.logger}"
case _ =>
s"❓ Unable to parse log: $logLine"
}
def main(args: Array[String]): Unit = {
val logLines = List(
"2023-12-25 10:30:45 [INFO] UserService: User login successful",
"2023-12-25 10:31:02 [ERROR] DatabaseService: Connection timeout",
"2023-12-25 10:31:15 [WARN] [main-thread] CacheService: Cache miss for key: user_123",
"2023-12-25 10:31:30 [ERROR] [worker-1] PaymentService: Payment processing failed",
"Invalid log format",
"2023-12-25 10:32:00 [DEBUG] SecurityService: Token validation passed"
)
println("Log analysis results:")
logLines.foreach(line => println(analyzeLogEntry(line)))
}
}#Best Practices
#Extractor Design Principles
object ExtractorBestPractices {
// 1. Keep extractors simple and focused
object EmailDomain {
def unapply(email: String): Option[String] = {
val atIndex = email.indexOf('@')
if (atIndex > 0 && atIndex < email.length - 1) {
Some(email.substring(atIndex + 1))
} else None
}
}
// 2. Provide meaningful return values
object Temperature {
def unapply(celsius: Double): Option[String] = celsius match {
case c if c < 0 => Some("Below freezing point")
case c if c < 10 => Some("Cold")
case c if c < 25 => Some("Cool")
case c if c < 35 => Some("Warm")
case _ => Some("Hot")
}
}
// 3. Consider performance, avoid complex computations
object FastPrime {
private val knownPrimes = Set(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31)
def unapply(n: Int): Boolean = {
if (n <= 31) knownPrimes.contains(n)
else isPrime(n) // Only do complex computation for large numbers
}
private def isPrime(n: Int): Boolean = {
if (n < 2) false
else !(2 to math.sqrt(n).toInt).exists(n % _ == 0)
}
}
// 4. Combine extractors
object ValidUser {
def unapply(input: (String, String, Int)): Option[(String, String, Int)] = {
val (name, email, age) = input
val validName = name.trim.nonEmpty && name.length >= 2
val validEmail = email.contains("@") && email.contains(".")
val validAge = age >= 0 && age <= 150
if (validName && validEmail && validAge) Some((name, email, age))
else None
}
}
// 5. Error handling
object SafeInt {
def unapply(s: String): Option[Int] = {
try {
Some(s.toInt)
} catch {
case _: NumberFormatException => None
}
}
}
def demonstrateBestPractices(): Unit = {
// Email domain extraction
val emails = List("user@gmail.com", "admin@company.org", "invalid-email")
emails.foreach {
case email @ EmailDomain(domain) => println(s"$email's domain is $domain")
case email => println(s"$email is not a valid email")
}
// Temperature classification
val temperatures = List(-5.0, 5.0, 20.0, 30.0, 40.0)
temperatures.foreach {
case temp @ Temperature(category) => println(s"${temp}°C is $category")
}
// Prime checking
val numbers = List(2, 4, 17, 25, 29)
numbers.foreach {
case n @ FastPrime() => println(s"$n is prime")
case n => println(s"$n is not prime")
}
// User validation
val users = List(
("Alice", "alice@example.com", 25),
("", "invalid", -5),
("Bob", "bob@test.com", 30)
)
users.foreach {
case ValidUser(name, email, age) => println(s"Valid user: $name, $email, $age")
case (name, email, age) => println(s"Invalid user: $name, $email, $age")
}
// Safe integer parsing
val numberStrings = List("123", "abc", "456")
numberStrings.foreach {
case SafeInt(number) => println(s"Parsing successful: $number")
case str => println(s"Parsing failed: $str")
}
}
def main(args: Array[String]): Unit = {
demonstrateBestPractices()
}
}#Summary
Extractors are the core mechanism of Scala pattern matching:
-
Basic Concepts:
unapplymethod defines how to extract values- Returns
Option[T]orBoolean unapplySeqfor variable number of values
-
Design Principles:
- Keep simple and focused
- Provide meaningful return values
- Consider performance impact
- Proper error handling
-
Application Scenarios:
- Data validation and parsing
- Pattern matching enhancement
- Domain-specific languages
- API design
-
Best Practices:
- Combine simple extractors
- Avoid side effects
- Consider type safety
- Provide clear documentation
Extractors make pattern matching more powerful and flexible, and are an important tool for functional programming in Scala.