The Apply Method: Making Objects Callable
Introduction
The apply
method is one of Scala's most distinctive and powerful features. It allows objects to be "called" like functions, creating elegant syntax for object creation, factory patterns, and data access. When you write List(1, 2, 3)
or Map("key" -> "value")
, you're using apply methods behind the scenes.
Understanding how to implement and use apply methods effectively will make your code more readable, your APIs more intuitive, and your factory patterns more elegant. This lesson will show you how to leverage apply methods for clean, expressive Scala code.
Basic Apply Method Implementation
Simple Apply Methods
class Greeter(val message: String) {
def greet(name: String): String = s"$message, $name!"
}
object Greeter {
// Apply method allows Greeter(...) syntax instead of new Greeter(...)
def apply(message: String): Greeter = new Greeter(message)
// Multiple apply methods with different signatures
def apply(): Greeter = new Greeter("Hello")
def apply(message: String, isPolite: Boolean): Greeter = {
val prefix = if (isPolite) "Good day" else "Hey"
new Greeter(s"$prefix, $message")
}
}
// Usage - all of these use apply methods
val greeter1 = Greeter("Welcome") // Calls apply(String)
val greeter2 = Greeter() // Calls apply()
val greeter3 = Greeter("there", false) // Calls apply(String, Boolean)
println(greeter1.greet("Alice")) // Welcome, Alice!
println(greeter2.greet("Bob")) // Hello, Bob!
println(greeter3.greet("Charlie")) // Hey, there, Charlie!
// You can still call apply explicitly
val greeter4 = Greeter.apply("Greetings")
println(greeter4.greet("David")) // Greetings, David!
Apply for Collection-Like Classes
class SafeList[T] private(private val items: List[T]) {
// Apply method for element access (like array/list indexing)
def apply(index: Int): Option[T] = {
if (index >= 0 && index < items.length) Some(items(index))
else None
}
def length: Int = items.length
def toList: List[T] = items
def add(item: T): SafeList[T] = new SafeList(items :+ item)
def remove(index: Int): SafeList[T] = {
if (index >= 0 && index < items.length) {
val (before, after) = items.splitAt(index)
new SafeList(before ++ after.tail)
} else this
}
override def toString: String = s"SafeList(${items.mkString(", ")})"
}
object SafeList {
// Factory apply methods
def apply[T](items: T*): SafeList[T] = new SafeList(items.toList)
def apply[T](list: List[T]): SafeList[T] = new SafeList(list)
def empty[T]: SafeList[T] = new SafeList(List.empty[T])
// Apply for creating from other collections
def apply[T](iterable: Iterable[T]): SafeList[T] = new SafeList(iterable.toList)
}
// Usage
val numbers = SafeList(1, 2, 3, 4, 5) // Factory apply
val fromList = SafeList(List(10, 20, 30)) // Factory apply from List
val empty = SafeList.empty[String] // Factory method
// Element access using apply
println(numbers(0)) // Some(1) - calls apply(Int)
println(numbers(2)) // Some(3) - calls apply(Int)
println(numbers(10)) // None - safe access
// Building up collections
val moreNumbers = numbers.add(6).add(7)
println(moreNumbers) // SafeList(1, 2, 3, 4, 5, 6, 7)
val lessNumbers = moreNumbers.remove(0)
println(lessNumbers) // SafeList(2, 3, 4, 5, 6, 7)
Apply with Validation
case class Email private(address: String) {
def domain: String = address.split("@")(1)
def localPart: String = address.split("@")(0)
def isGmail: Boolean = domain.toLowerCase == "gmail.com"
override def toString: String = address
}
object Email {
// Apply method with validation
def apply(address: String): Either[String, Email] = {
validate(address).map(new Email(_))
}
// Unsafe apply that throws exceptions
def unsafeApply(address: String): Email = {
apply(address) match {
case Right(email) => email
case Left(error) => throw new IllegalArgumentException(error)
}
}
private def validate(address: String): Either[String, String] = {
val trimmed = address.trim.toLowerCase
if (trimmed.isEmpty) {
Left("Email cannot be empty")
} else if (!trimmed.contains("@")) {
Left("Email must contain @")
} else if (trimmed.startsWith("@") || trimmed.endsWith("@")) {
Left("Email cannot start or end with @")
} else if (trimmed.count(_ == '@') != 1) {
Left("Email must contain exactly one @")
} else {
val parts = trimmed.split("@")
if (parts(0).isEmpty) Left("Local part cannot be empty")
else if (parts(1).isEmpty) Left("Domain cannot be empty")
else if (!parts(1).contains(".")) Left("Domain must contain a dot")
else Right(trimmed)
}
}
// Convenience factory methods
def gmail(localPart: String): Either[String, Email] = apply(s"$localPart@gmail.com")
def yahoo(localPart: String): Either[String, Email] = apply(s"$localPart@yahoo.com")
def outlook(localPart: String): Either[String, Email] = apply(s"$localPart@outlook.com")
}
// Usage
Email("alice@example.com") match {
case Right(email) => println(s"Valid email: $email (domain: ${email.domain})")
case Left(error) => println(s"Invalid email: $error")
}
Email("invalid-email") match {
case Right(email) => println(s"Valid email: $email")
case Left(error) => println(s"Invalid email: $error")
}
// Convenience factories
Email.gmail("john.doe") match {
case Right(email) => println(s"Gmail: $email")
case Left(error) => println(s"Error: $error")
}
// Batch creation
val emailStrings = List("alice@example.com", "bob@invalid", "charlie@test.org")
val emails = emailStrings.map(Email.apply)
emails.foreach {
case Right(email) => println(s"✓ $email")
case Left(error) => println(s"✗ $error")
}
Advanced Apply Patterns
Configuration Builders with Apply
case class DatabaseConfig(
host: String,
port: Int,
database: String,
username: String,
password: String,
maxConnections: Int = 10,
timeout: Int = 30000,
ssl: Boolean = false
)
object DatabaseConfig {
// Apply method for most common case
def apply(host: String, database: String, username: String, password: String): DatabaseConfig = {
new DatabaseConfig(host, 5432, database, username, password) // Default PostgreSQL port
}
// Apply for MySQL with default port
def mysql(host: String, database: String, username: String, password: String): DatabaseConfig = {
new DatabaseConfig(host, 3306, database, username, password)
}
// Apply for local development
def local(database: String): DatabaseConfig = {
new DatabaseConfig("localhost", 5432, database, "dev", "dev")
}
// Apply from environment variables
def fromEnv(): Option[DatabaseConfig] = {
for {
host <- sys.env.get("DB_HOST")
port <- sys.env.get("DB_PORT").flatMap(p => scala.util.Try(p.toInt).toOption)
database <- sys.env.get("DB_NAME")
username <- sys.env.get("DB_USER")
password <- sys.env.get("DB_PASS")
} yield {
val maxConn = sys.env.get("DB_MAX_CONN").flatMap(c => scala.util.Try(c.toInt).toOption).getOrElse(10)
val timeout = sys.env.get("DB_TIMEOUT").flatMap(t => scala.util.Try(t.toInt).toOption).getOrElse(30000)
val ssl = sys.env.get("DB_SSL").exists(_.toLowerCase == "true")
DatabaseConfig(host, port, database, username, password, maxConn, timeout, ssl)
}
}
// Apply from configuration map
def fromMap(config: Map[String, String]): Either[String, DatabaseConfig] = {
def getRequired(key: String): Either[String, String] = {
config.get(key).toRight(s"Missing required configuration: $key")
}
def getInt(key: String, default: Int): Either[String, Int] = {
config.get(key) match {
case Some(value) => scala.util.Try(value.toInt).toEither.left.map(_ => s"Invalid integer for $key: $value")
case None => Right(default)
}
}
for {
host <- getRequired("host")
port <- getInt("port", 5432)
database <- getRequired("database")
username <- getRequired("username")
password <- getRequired("password")
maxConn <- getInt("maxConnections", 10)
timeout <- getInt("timeout", 30000)
} yield {
val ssl = config.get("ssl").exists(_.toLowerCase == "true")
DatabaseConfig(host, port, database, username, password, maxConn, timeout, ssl)
}
}
}
// Usage
val prodConfig = DatabaseConfig("prod-db.company.com", "myapp", "produser", "prodpass")
val mysqlConfig = DatabaseConfig.mysql("mysql.company.com", "myapp", "user", "pass")
val devConfig = DatabaseConfig.local("myapp_dev")
println(s"Production: ${prodConfig.host}:${prodConfig.port}")
println(s"MySQL: ${mysqlConfig.host}:${mysqlConfig.port}")
println(s"Development: ${devConfig.host}:${devConfig.port}")
// Configuration from map
val configMap = Map(
"host" -> "staging-db.company.com",
"port" -> "5432",
"database" -> "myapp_staging",
"username" -> "staging_user",
"password" -> "staging_pass",
"ssl" -> "true"
)
DatabaseConfig.fromMap(configMap) match {
case Right(config) => println(s"Staging config: ${config.host}, SSL: ${config.ssl}")
case Left(error) => println(s"Configuration error: $error")
}
Apply for Parser Combinators
// Simple expression parser using apply methods
sealed trait Expr
case class Num(value: Double) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Mul(left: Expr, right: Expr) extends Expr
case class Var(name: String) extends Expr
object Expr {
// Apply method for creating numbers
def apply(value: Double): Expr = Num(value)
def apply(value: Int): Expr = Num(value.toDouble)
// Apply method for creating variables
def apply(name: String): Expr = Var(name)
// Apply methods for operations
def add(left: Expr, right: Expr): Expr = Add(left, right)
def mul(left: Expr, right: Expr): Expr = Mul(left, right)
}
// Operators using apply methods
object + {
def apply(left: Expr, right: Expr): Expr = Add(left, right)
}
object * {
def apply(left: Expr, right: Expr): Expr = Mul(left, right)
}
// Expression evaluator
object ExprEvaluator {
def apply(expr: Expr, variables: Map[String, Double] = Map.empty): Either[String, Double] = {
expr match {
case Num(value) => Right(value)
case Var(name) =>
variables.get(name).toRight(s"Variable '$name' not found")
case Add(left, right) =>
for {
l <- apply(left, variables)
r <- apply(right, variables)
} yield l + r
case Mul(left, right) =>
for {
l <- apply(left, variables)
r <- apply(right, variables)
} yield l * r
}
}
}
// Pretty printer
object ExprPrinter {
def apply(expr: Expr): String = expr match {
case Num(value) => value.toString
case Var(name) => name
case Add(left, right) => s"(${apply(left)} + ${apply(right)})"
case Mul(left, right) => s"(${apply(left)} * ${apply(right)})"
}
}
// Usage - building expressions with apply methods
val expr1 = Expr.add(Expr(5), Expr(3)) // 5 + 3
val expr2 = Expr.mul(Expr("x"), Expr(2)) // x * 2
val expr3 = +(*(Expr("x"), Expr(2)), Expr(10)) // (x * 2) + 10
// Alternative syntax using object operators
val expr4 = +(Expr(5), *(Expr("y"), Expr(3))) // 5 + (y * 3)
println(s"Expression 1: ${ExprPrinter(expr1)}")
println(s"Expression 2: ${ExprPrinter(expr2)}")
println(s"Expression 3: ${ExprPrinter(expr3)}")
println(s"Expression 4: ${ExprPrinter(expr4)}")
// Evaluation
val variables = Map("x" -> 7.0, "y" -> 4.0)
List(expr1, expr2, expr3, expr4).foreach { expr =>
ExprEvaluator(expr, variables) match {
case Right(result) => println(s"${ExprPrinter(expr)} = $result")
case Left(error) => println(s"${ExprPrinter(expr)} -> Error: $error")
}
}
Multi-Parameter Apply Methods
class Matrix private(val rows: Int, val cols: Int, private val data: Array[Array[Double]]) {
// Apply for element access
def apply(row: Int, col: Int): Double = {
require(row >= 0 && row < rows, s"Row index $row out of bounds [0, $rows)")
require(col >= 0 && col < cols, s"Column index $col out of bounds [0, $cols)")
data(row)(col)
}
// Update method (companion to apply)
def update(row: Int, col: Int, value: Double): Unit = {
require(row >= 0 && row < rows, s"Row index $row out of bounds [0, $rows)")
require(col >= 0 && col < cols, s"Column index $col out of bounds [0, $cols)")
data(row)(col) = value
}
def +(other: Matrix): Matrix = {
require(rows == other.rows && cols == other.cols, "Matrix dimensions must match")
val result = Matrix.zeros(rows, cols)
for (i <- 0 until rows; j <- 0 until cols) {
result(i, j) = this(i, j) + other(i, j)
}
result
}
def *(scalar: Double): Matrix = {
val result = Matrix.zeros(rows, cols)
for (i <- 0 until rows; j <- 0 until cols) {
result(i, j) = this(i, j) * scalar
}
result
}
override def toString: String = {
data.map(_.mkString("[", ", ", "]")).mkString("Matrix(\n ", ",\n ", "\n)")
}
}
object Matrix {
// Apply method for creating from nested sequences
def apply(data: Seq[Seq[Double]]): Matrix = {
require(data.nonEmpty, "Matrix cannot be empty")
require(data.forall(_.length == data.head.length), "All rows must have the same length")
val rows = data.length
val cols = data.head.length
val array = data.map(_.toArray).toArray
new Matrix(rows, cols, array)
}
// Apply method for creating from flat sequence with dimensions
def apply(rows: Int, cols: Int, data: Seq[Double]): Matrix = {
require(data.length == rows * cols, s"Data length ${data.length} doesn't match dimensions ${rows}x${cols}")
val array = Array.ofDim[Double](rows, cols)
for (i <- 0 until rows; j <- 0 until cols) {
array(i)(j) = data(i * cols + j)
}
new Matrix(rows, cols, array)
}
// Apply method for creating from varargs
def apply(rows: Int, cols: Int)(data: Double*): Matrix = {
apply(rows, cols, data)
}
// Factory methods
def zeros(rows: Int, cols: Int): Matrix = {
val data = Array.fill(rows, cols)(0.0)
new Matrix(rows, cols, data)
}
def ones(rows: Int, cols: Int): Matrix = {
val data = Array.fill(rows, cols)(1.0)
new Matrix(rows, cols, data)
}
def identity(size: Int): Matrix = {
val result = zeros(size, size)
for (i <- 0 until size) {
result(i, i) = 1.0
}
result
}
def random(rows: Int, cols: Int): Matrix = {
val rand = scala.util.Random
val data = Array.fill(rows, cols)(rand.nextGaussian())
new Matrix(rows, cols, data)
}
}
// Usage
val matrix1 = Matrix(Seq(
Seq(1.0, 2.0, 3.0),
Seq(4.0, 5.0, 6.0)
))
val matrix2 = Matrix(2, 3, Seq(1.0, 0.0, -1.0, 2.0, -1.0, 3.0))
val matrix3 = Matrix(2, 3)(
1.0, 2.0, 3.0,
4.0, 5.0, 6.0
)
println("Matrix 1:")
println(matrix1)
println(s"Element at (1, 2): ${matrix1(1, 2)}") // Using apply for access
println("\nMatrix 2:")
println(matrix2)
// Matrix operations
val sum = matrix1 + matrix2
val scaled = matrix1 * 2.0
println("\nSum:")
println(sum)
println("\nScaled by 2:")
println(scaled)
// Factory methods
val zeros = Matrix.zeros(3, 3)
val identity = Matrix.identity(3)
val random = Matrix.random(2, 2)
println("\nZeros 3x3:")
println(zeros)
println("\nIdentity 3x3:")
println(identity)
println("\nRandom 2x2:")
println(random)
// Mutable updates using update method
val mutable = Matrix.zeros(2, 2)
mutable(0, 0) = 1.0 // Uses update method
mutable(1, 1) = 2.0 // Uses update method
println("\nMutable after updates:")
println(mutable)
Apply with Pattern Matching
Custom Extractors using Apply/Unapply
class Coordinate(val x: Double, val y: Double) {
def distanceFromOrigin: Double = math.sqrt(x * x + y * y)
def distanceTo(other: Coordinate): Double = {
val dx = x - other.x
val dy = y - other.y
math.sqrt(dx * dx + dy * dy)
}
override def toString: String = s"($x, $y)"
}
object Coordinate {
// Apply method for creation
def apply(x: Double, y: Double): Coordinate = new Coordinate(x, y)
// Unapply method for pattern matching
def unapply(coord: Coordinate): Option[(Double, Double)] = {
Some((coord.x, coord.y))
}
// Named extractors using apply/unapply pattern
object Origin {
def apply(): Coordinate = new Coordinate(0, 0)
def unapply(coord: Coordinate): Boolean = coord.x == 0 && coord.y == 0
}
object Quadrant {
object I {
def unapply(coord: Coordinate): Boolean = coord.x > 0 && coord.y > 0
}
object II {
def unapply(coord: Coordinate): Boolean = coord.x < 0 && coord.y > 0
}
object III {
def unapply(coord: Coordinate): Boolean = coord.x < 0 && coord.y < 0
}
object IV {
def unapply(coord: Coordinate): Boolean = coord.x > 0 && coord.y < 0
}
}
object OnAxis {
def unapply(coord: Coordinate): Option[String] = {
if (coord.x == 0 && coord.y != 0) Some("Y-axis")
else if (coord.y == 0 && coord.x != 0) Some("X-axis")
else if (coord.x == 0 && coord.y == 0) Some("Origin")
else None
}
}
object Distance {
def unapply(coord: Coordinate): Option[Double] = {
Some(coord.distanceFromOrigin)
}
}
}
// Usage with pattern matching
def analyzeCoordinate(coord: Coordinate): String = coord match {
case Coordinate.Origin() =>
"Point is at the origin"
case Coordinate.OnAxis(axis) =>
s"Point is on the $axis"
case Coordinate.Quadrant.I() =>
"Point is in Quadrant I (positive x, positive y)"
case Coordinate.Quadrant.II() =>
"Point is in Quadrant II (negative x, positive y)"
case Coordinate.Quadrant.III() =>
"Point is in Quadrant III (negative x, negative y)"
case Coordinate.Quadrant.IV() =>
"Point is in Quadrant IV (positive x, negative y)"
case Coordinate.Distance(d) if d > 10 =>
f"Point is far from origin (distance: $d%.2f)"
case Coordinate(x, y) =>
f"Point at ($x%.1f, $y%.1f)"
}
// Test coordinates
val testPoints = List(
Coordinate(0, 0), // Origin
Coordinate(0, 5), // Y-axis
Coordinate(-3, 0), // X-axis
Coordinate(2, 3), // Quadrant I
Coordinate(-2, 3), // Quadrant II
Coordinate(-2, -3), // Quadrant III
Coordinate(2, -3), // Quadrant IV
Coordinate(8, 6), // Far point
Coordinate(1.5, 2.7) // Regular point
)
testPoints.foreach { point =>
println(s"$point: ${analyzeCoordinate(point)}")
}
// Using apply for creation
val origin = Coordinate.Origin() // apply method
val point1 = Coordinate(3.0, 4.0) // apply method
val point2 = Coordinate(-1.0, 2.0) // apply method
println(s"\nDistances:")
println(s"Origin to Point1: ${origin.distanceTo(point1)}")
println(s"Point1 to Point2: ${point1.distanceTo(point2)}")
Practical Examples
Configuration DSL with Apply
// Server configuration DSL using apply methods
case class ServerConfig(
host: String = "localhost",
port: Int = 8080,
ssl: Boolean = false,
threads: Int = 10,
timeout: Int = 30000,
routes: List[Route] = List.empty
)
case class Route(path: String, method: String, handler: String)
object Server {
private var currentConfig = ServerConfig()
// Apply method for starting configuration
def apply(): ServerConfigBuilder = new ServerConfigBuilder(currentConfig)
// Apply method with immediate host setting
def apply(host: String): ServerConfigBuilder = new ServerConfigBuilder(currentConfig.copy(host = host))
// Apply method with host and port
def apply(host: String, port: Int): ServerConfigBuilder = {
new ServerConfigBuilder(currentConfig.copy(host = host, port = port))
}
}
class ServerConfigBuilder(private var config: ServerConfig) {
def host(h: String): ServerConfigBuilder = {
config = config.copy(host = h)
this
}
def port(p: Int): ServerConfigBuilder = {
config = config.copy(port = p)
this
}
def ssl(enabled: Boolean = true): ServerConfigBuilder = {
config = config.copy(ssl = enabled)
this
}
def threads(count: Int): ServerConfigBuilder = {
config = config.copy(threads = count)
this
}
def timeout(ms: Int): ServerConfigBuilder = {
config = config.copy(timeout = ms)
this
}
def route(path: String, method: String, handler: String): ServerConfigBuilder = {
val newRoute = Route(path, method, handler)
config = config.copy(routes = config.routes :+ newRoute)
this
}
def build(): ServerConfig = config
def start(): String = {
val protocol = if (config.ssl) "https" else "http"
val routeInfo = config.routes.map(r => s" ${r.method} ${r.path} -> ${r.handler}").mkString("\n")
s"""Server started:
| URL: $protocol://${config.host}:${config.port}
| Threads: ${config.threads}
| Timeout: ${config.timeout}ms
| Routes:
|$routeInfo""".stripMargin
}
}
// Route DSL using apply methods
object GET {
def apply(path: String, handler: String): Route = Route(path, "GET", handler)
}
object POST {
def apply(path: String, handler: String): Route = Route(path, "POST", handler)
}
object PUT {
def apply(path: String, handler: String): Route = Route(path, "PUT", handler)
}
object DELETE {
def apply(path: String, handler: String): Route = Route(path, "DELETE", handler)
}
// Enhanced server with route DSL
object EnhancedServer {
def apply(): EnhancedServerBuilder = new EnhancedServerBuilder(ServerConfig())
}
class EnhancedServerBuilder(private var config: ServerConfig) {
def host(h: String): EnhancedServerBuilder = {
config = config.copy(host = h)
this
}
def port(p: Int): EnhancedServerBuilder = {
config = config.copy(port = p)
this
}
def ssl(): EnhancedServerBuilder = {
config = config.copy(ssl = true)
this
}
def routes(routeList: Route*): EnhancedServerBuilder = {
config = config.copy(routes = config.routes ++ routeList)
this
}
def start(): String = {
val protocol = if (config.ssl) "https" else "http"
val routeInfo = config.routes.map(r => s" ${r.method} ${r.path} -> ${r.handler}").mkString("\n")
s"""Enhanced Server started:
| URL: $protocol://${config.host}:${config.port}
| Routes:
|$routeInfo""".stripMargin
}
}
// Usage examples
println("=== Basic Server Configuration ===")
val server1 = Server()
.host("api.company.com")
.port(443)
.ssl(true)
.threads(20)
.timeout(60000)
.route("/users", "GET", "UserController.list")
.route("/users", "POST", "UserController.create")
.route("/users/:id", "GET", "UserController.get")
.start()
println(server1)
println("\n=== Server with Constructor Apply ===")
val server2 = Server("localhost", 3000)
.ssl()
.route("/health", "GET", "HealthController.check")
.route("/metrics", "GET", "MetricsController.get")
.start()
println(server2)
println("\n=== Enhanced Server with Route DSL ===")
val server3 = EnhancedServer()
.host("api.example.com")
.port(8080)
.routes(
GET("/", "HomeController.index"),
GET("/users", "UserController.list"),
POST("/users", "UserController.create"),
PUT("/users/:id", "UserController.update"),
DELETE("/users/:id", "UserController.delete"),
GET("/products", "ProductController.list"),
POST("/products", "ProductController.create")
)
.start()
println(server3)
Summary
In this lesson, you've mastered the apply method and its many applications:
✅ Basic Apply: Creating function-like syntax for object instantiation
✅ Collection Access: Using apply for element access and indexing
✅ Factory Patterns: Elegant object creation with validation
✅ DSL Creation: Building domain-specific languages with apply methods
✅ Pattern Matching: Combining apply/unapply for custom extractors
✅ API Design: Creating intuitive, readable interfaces
The apply method is one of Scala's most powerful features for creating clean, expressive code that feels natural to use.
What's Next
Congratulations! You've completed the core object-oriented programming section of the Scala course. You've learned about classes, objects, inheritance, traits, case classes, packages, visibility control, and the apply method.
In the next part of the course, we'll dive into functional programming concepts including immutability, higher-order functions, collections, pattern matching, and monads. These concepts will complement your OOP knowledge and show you how Scala seamlessly blends both paradigms.
Ready to explore functional programming? Let's continue your Scala journey!
Comments
Be the first to comment on this lesson!