12 minutes read

We already know about objects in Scala. But did you know that there are special objects that allow you to make the code more concise and structured? These objects are known as companion objects. Let's see how to define them and what possibilities they provide for our traits, classes, and abstract classes.

Companions

They are called companions because they must be declared in the same file as a class (or trait) and have the same name as the class. A companion object has several purposes, and one of them is constants. Also, a companion object is useful for implementing helper methods. Using this approach lets you create constants that you may need, as shown in this example:

class Pizza(val crustType: String):
  override def toString = s"Pizza with a $crustType crust"

// Companion object
object Pizza:
  object CrustType:
    val Thin = "thin"
    val Thick = "thick"

  def cookManyPizza(amount: Int, crustType: String): List[Pizza] =
    List.fill(amount)(Pizza(crustType))

The Pizza class and the Pizza object are defined in the same file, and members of the object can be used without initializing a specific pizza:

println(Pizza.CrustType.Thin) // thin
println(Pizza.cookManyPizza(3, Pizza.CrustType.Thick)) // list of three pizzas

You can also create a new Pizza instance and use it as usual:

println(Pizza(Pizza.CrustType.Thick)) // Pizza with a thick crust

It's also important to know that a class and its companion object can access each other's private members. Let's imagine that our pizza has a list of toppings that makes it tastier than our competitors' pizza. But we also have a special secret sauce. And to protect it we made the private field of class Pizza. Our secret sauce will be in the object because other Pizza functions can use it, but the field is available in the scope of the class.

class Pizza(val crustType: String, private val sauces: List[String] = List.empty):
  override def toString = s"Crust type is $crustType"

  def addSecretSauce: Pizza = new Pizza(crustType, Pizza.SecretSauce :: sauces)

object Pizza:
  private val SecretSauce = "Tabasco"

  object CrustType:
    val Thin = "thin"
    val Thick = "thick"

IntelliJ IDEA in the example above shows us a hint in symbols that the class has a companion object.

ascreenshot of the Intellij idea shows us a hint in symbols that the class has a companion object

The apply method

It is possible to create an instance of a class without the new keyword (for Scala 2). It adds flexibility and readability to instance creation. To use this approach, you have to define the apply method in the companion object.

trait Shape:
  def area: Double

object Shape:
  private class Circle(radius: Double) extends Shape:
    override val area = 3.14 * radius * radius

  private class Rectangle(height: Double, width: Double) extends Shape:
    override val area = height * width
  
  def apply(height: Double, width: Double): Shape = Rectangle(height, length)
  def apply(radius: Double): Shape = Circle(radius)

In Scala 3 we don't have to use new keyword, but anyway, two and more definitions of the apply method with a varying number of parameters allow you to create instances in different ways.

val circle = Shape(radius = 2)
println(circle.area) // 12.56

val rectangle = Shape(height = 2, width = 3)
println(rectangle.area) // 6.0

givens

All givens (or implicits in Scala 2), which are implemented in the companion object, will be imported together with the class. This makes it easier to work with implicit mechanics, because we can create the necessary implicit functions or variables for each class separately and not worry about importing them.

As you know, American unit systems differ from European ones. So let's implement the implicit conversions between these systems and try to use them without importing them.

// systems.Inches.scala

class Inches(val value: Double)
object Inches:
  given Conversion[Inches, Centimeters] = inches => Centimeters(inches.value * 2.54)


// systems.Centimeters.scala

class Centimeters(val value: Double)
object Centimeters:
  given Conversion[Centimeters, Inches] = centimeters => Inches(centimeters.value / 2.54)


// Main.scala

import systems.{Inches, Centimeters}

object Main extends App:
  val centimeters: Centimeters = Inches(100) // Centimeters(254)
  val inches: Inches = centimeters // Inches(100)

The Scala compiler knows that Inches is not a subtype of Centimeters. However, instead of a compilation error, it checks if there is an implicit conversion available.

Conclusion

Now you have an idea why they are called "companions" and how they keep the code concise. You also learned that objects together with a class are imported with static method and values. Besides, a companion object and its class can access each other's private members.

4 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo