Package objects

Versions: Scala 2.12.1

Who didn't encounter a question about helper classes ? For ones, creating them isn't legitimate since everything we can link to an object. For the others they're fully legal because they help to keep code base understandable. Scala comes with an idea that can make both sides agree - package objects.

Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I'm currently writing one on that topic and the first chapters are already available in πŸ‘‰ Early Release on the O'Reilly platform

I also help solve your data engineering problems πŸ‘‰ contact@waitingforcode.com πŸ“©

This post talks about package objects in Scala. As often, its first section starts with a definition of the concept. The second one gives its specific use cases while the last one shows what happens after the compilation.

Package objects definition

Very often in our applications we have a dilemma about where to put all properties and methods potentially accessible from any other classes in the package. Scala shows a direction to solve this issue and suggests to use package objects. They're a kind of containers that store the declarations shared by package members. We'll discuss the use cases in the second section of this post.

The package object is a Scala file located in the package that it corresponds to. For instance, if we want to put a package object for com.waitingforcode.core.packageobjects package, we'll need to declare it as:

package com.waitingforcode.core

package object packageobjects {

}

As you can notice, the declaration looks very similarly to the declaration of Scala's objects. The difference is that the definition is preceded with package keyword. Any definitions from package object are considered members of the package itself.

You must also know that package objects are closed. It means that each package can contain only 1 package object.

Use cases

To see package object use cases we can take a look at Scala's package.scala file:

package object scala {
  type Throwable = java.lang.Throwable
  type Exception = java.lang.Exception
  // ...
  val List = scala.collection.immutable.List

As you can see, a package object can contain: fields, functions, type aliases or enumerations. Below you can find an example of package object and the tests showing that it works:

package com.waitingforcode.core

import scala.collection.mutable

package object packageobjects {

  type GamePlayers = Map[String, Int]

  def convertToGamePlayers(serializedPlayers: String): GamePlayers = {
    serializedPlayers.split(",")
      .map(player => (player, 0))
      .toMap
  }

  object GameResults extends Enumeration {
    type GameResult = Value
    val WIN, LOSE = Value
  }

  val CachedDocuments = new mutable.HashMap[String, String]()

}

Using the elements declared in the package object it's easy since we can refer to them directly when the caller is at the same level. Otherwise we need to import them as packageobjects would be a static object (e.g. import com.waitingforcode.core.packageobjects.CachedDocuments) :

class PackageObjectsTest extends FlatSpec with Matchers {

  "a method" should "use field declared in package object" in {
    def getDocument(documentId: String): String = {
      val loadedDocument = CachedDocuments.get(documentId).getOrElse("some loaded document")
      CachedDocuments.put(documentId, loadedDocument)
      loadedDocument
    }

    val document = getDocument("X")

    document shouldEqual "some loaded document"
    CachedDocuments should contain key("X")
  }

  "a code" should "use method declared in package object" in {
    val playersToConvert = "player1,player2,player3"

    val playersWithPoints = convertToGamePlayers(playersToConvert)

    playersWithPoints should have size 3
    playersWithPoints should contain allOf(("player1", 0), ("player2", 0), ("player3", 0))
  }

  "a code" should "use type alias defined in package object" in {
    val players: GamePlayers = Map("player1" -> 1, "player2" -> 2)

    players should have size 2
    players should contain allOf(("player1", 1), ("player2", 2))
  }

  "a conditional expression" should "use enumeration from package object" in {
    val result = GameResults.WIN
    val label = if (result == GameResults.WIN) {
      "winner"
    } else {
      "looser"
    }

    label shouldEqual "winner"
  }

}

Practically we can also add a class declaration to package object. If we do so and compile the code with -Xlint option, the compiler will warn us against bad practice:

Warning:(5, 9) it is not recommended to define classes/objects inside of package objects.
If possible, define class SomeClass in package othertest instead.
  class SomeClass {

Aside of linter warns we should avoid to mix all classes definition inside package object because of readability. It's always clearer to group classes in files those names indicate what the stored classes do. Looking for this in a big package object file would be impractical. As already told we can have exactly one package object per package. Having more than one could lead into an unpredictable executions - sometimes the JVM can pick "wrong" class. Thus, it's better to define the classes in separate files instead of putting them into package object.

Package object decompiled

To get a better idea about what happens under-the-hood, let's use the same package object as in above tests and do simple javap on compiled classes.

The result of javap command for package.class file:

javap target/scala-2.12/classes/com/waitingforcode/core/packageobjects/package.class
Compiled from "package.scala"
public final class com.waitingforcode.core.packageobjects.package {
  public static scala.collection.mutable.HashMap<java.lang.String, java.lang.String> CachedDocuments();
  public static scala.collection.immutable.Map<java.lang.String, java.lang.Object> convertToGamePlayers(java.lang.String);
}

As you can see, the generated code is a final class exposing 2 methods: one returning cache for loaded documents and another for conversion from stringified players to their object representation. Both represent basic structure of the original source file (picked data). To see everything we defined in package object, we need to disassemble package$.class:

javap target/scala-2.12/classes/com/waitingforcode/core/packageobjects/package$.class
Compiled from "package.scala"
public final class com.waitingforcode.core.packageobjects.package$ {
  public static com.waitingforcode.core.packageobjects.package$ MODULE$;
  public static {};
  public scala.collection.immutable.Map<java.lang.String, java.lang.Object> convertToGamePlayers(java.lang.String);
  public scala.collection.mutable.HashMap<java.lang.String, java.lang.String> CachedDocuments();
  public static final scala.Tuple2 $anonfun$convertToGamePlayers$1(java.lang.String);
}

As you can notice, the enumeration is not included in above result. It's because it was compiled to a separate .class file called package$GameResults$.class:

javap target/scala-2.12/classes/com/waitingforcode/core/packageobjects/package\$GameResults\$.class
Compiled from "package.scala"
public class com.waitingforcode.core.packageobjects.package$GameResults$ extends scala.Enumeration {
  public static com.waitingforcode.core.packageobjects.package$GameResults$ MODULE$;
  public static {};
  public scala.Enumeration$Value WIN();
  public scala.Enumeration$Value LOSE();
  public com.waitingforcode.core.packageobjects.package$GameResults$();
}

Package object are an ideal place to store all top level data (variables, aliases, general functions) shared by given package members. It's not a good practice to use this place to store class definitions. It degrades code readability and will sooner or later transform the package to a long spaghetti code. A package object is defined in the same level as the package that it references and is called as the last level of the package namespace. Despite its quite exotic construction, it's compiled as any other regular class.