Scala Companion Object — A Deep Dive

Ani
4 min readSep 1, 2024

--

To get the full value of joy you must have someone to divide it with. ― Mark Twain

Companion

In Scala, a companion object is an object that is declared in the same file as a class and has the same name as that class. Companion objects and their corresponding classes can access each other’s private members, providing a powerful way to encapsulate code and share data between instances and static contexts.

Key Features of Companion Objects :

  1. Shared Name: The companion object and its class share the same name.
  2. Same File: They must be defined in the same source file.
  3. Access to Private Members: The companion object and the class can access each other’s private fields and methods.
  4. Singleton: The companion object is a singleton, meaning there is only one instance of it.

Benefits of Using Companion Objects :

  1. Factory Methods: They can be used to create factory methods that provide more control over object creation.
  2. Utility Methods: They can store utility functions that are related to the class but don’t need to be tied to a specific instance.
  3. Encapsulation: They allow private members to be shared between the class and the companion object, enhancing encapsulation.

Example :

Below is a detailed example that demonstrates these features and benefits.

class Person(private val name: String, private val age: Int) {
// Method to print person's info
def printInfo(): Unit = {
println(s"Name: $name, Age: $age")
}
}

object Person {
// Factory method to create a new Person
def apply(name: String, age: Int): Person = new Person(name, age)

// Utility method to check if a person is an adult
def isAdult(person: Person): Boolean = person.age >= 18

def main(args: Array[String]): Unit = {
// Creating an instance using the factory method
val john = Person("John", 25)

// Accessing the method from the class
john.printInfo() // Output: Name: John, Age: 25

// Using the utility method from the companion object
println(s"Is John an adult? ${Person.isAdult(john)}") // Output: Is John an adult? true
}
}

Detailed Explanation :

  1. Class Definition: The Person class has private members name and age, and a method printInfo that prints the person's information.
  2. Companion Object: The Person object serves as a companion to the Person class. It contains a factory method apply for creating instances of the class, and a utility method isAdult to check if a person is an adult.
  3. Access to Private Members: The companion object can access the private members of the class, enabling it to implement methods like isAdult that depend on private fields of Person.

Factory Methods :

Using the apply method in the companion object allows for a cleaner and more intuitive way to create instances of the class:

val john = Person("John", 25)

Utility Methods :

Utility methods like isAdult can be defined in the companion object, making them accessible without needing an instance of the clas

println(s"Is John an adult? ${Person.isAdult(john)}")

Singleton :

The companion object is a singleton, meaning there is only one instance of it. This is useful for holding state or methods that should be globally accessible.

Access Control :

Companion objects can access private fields and methods of their companion classes, allowing for tight integration and encapsulation of related functionality.

Advanced Usage Patterns :

Implicit Conversions :

Companion objects can also be used to define implicit conversions, making it easier to work with types that are logically related.

class Rectangle(val width: Double, val height: Double) {
def area: Double = width * height
}

object Rectangle {
implicit def tupleToRectangle(tuple: (Double, Double)): Rectangle = new Rectangle(tuple._1, tuple._2)
}

object Main extends App {
import Rectangle._

val rect: Rectangle = (5.0, 10.0)
println(s"Area: ${rect.area}") // Output: Area: 50.0
}

In this example, the tupleToRectangle implicit conversion allows a tuple to be automatically converted into a Rectangleobject.

Type Classes :

Companion objects are useful in implementing type classes, which provide a way to extend existing libraries with new functionality without modifying their code.

In this example, the Printable type class provides a way to define a format method for different types. The companion object Printable includes a method to use the type class instances.

trait Printable[A] {
def format(value: A): String
}

object PrintableInstances {
implicit val stringPrintable: Printable[String] = new Printable[String] {
def format(value: String): String = value
}

implicit val intPrintable: Printable[Int] = new Printable[Int] {
def format(value: Int): String = value.toString
}
}

object Printable {
def format[A](value: A)(implicit p: Printable[A]): String = p.format(value)
}

object Main extends App {
import PrintableInstances._

println(Printable.format("Hello")) // Output: Hello
println(Printable.format(123)) // Output: 123
}

Conclusion :

Companion objects in Scala offer a range of benefits, from simplifying object creation with factory methods to providing a convenient place for utility functions. They enhance encapsulation and allow for sophisticated patterns like implicit conversions and type classes. By leveraging companion objects, Scala developers can write more modular, maintainable, and expressive code.

For any type of help regarding career counselling, resume building, discussing designs or know more about latest data engineering trends and technologies reach out to me at anigos.

P.S : I don’t charge money

--

--

Ani

Senior Software Engineer, Big Data — Passionate about designing robust distributed systems