Before we look into value classes, lets start by why we might need them in the first place.

case class Client(id: Int, name: String)
case class BankAccount(id: Int, clientId: Int, amount: Double)

val account1 = BankAccount(1,1,100)
val account2 = BankAccount(2,1,200)
val account3 = BankAccount(1,2,1000)

val client1 = Client(1,"Danny")
val client2 = Client(2, "Emily")

In this example, we have three bank accounts, two belonging to Danny and one to Emily.

Now we add a function that checks if two bank accounts belong to the same client:

def isSameOwner(bankAccountId1: Int, bankAccountId2: Int): Boolean = ???

Now if we were to call the function with these parameters, the code will compile normally:

isSameOwner(account1.id, client1.id)
//or even
isSameOwner(account1.id, account2.clientId)

The reason this compiles is obvious, the function isSameOwner takes Int for parameters.

One possible solution would be to use tiny types in scala:

case class ClientId(id: Int)
case class AccountId(id: Int)

case class Client(clientId: ClientId, name: String)
case class BankAccount(accountId: AccountId, clientId: ClientId, amount: Double)

def isSameOwner(account1: AccountId, account2: AccountId): Boolean = ???

The issue with this solutions becomes memory allocation at runtime.

Which is where value classes come in. The main benefit is avoiding allocating runtime objects. At compile time, we would have the case case and at runtime we would have the underlying data type.

So what are value classes officially?

Value classes are a new mechanism in Scala to avoid allocating runtime objects. This is accomplished through the definition of new AnyVal subclasses. (https://docs.scala-lang.org/overviews/core/value-classes.html)

This means that value classes:

  1. Can only extend universal traits (ones that extend Any) and cannot be extended itself.

  2. Can define a def but no val or var or classes…

lets look at a very simple example:

class User(val userId: Int) extends AnyVal

As we can see in this example, value classes contain one simple underlying data type and extend AnyVal.

Now lets look at the byte code generated by our example:

public int fares();

This practically means that at compile time we can use the class User, but are runtime, the value class is replaced by its underlying value of type int.

So How is this useful?

instead of writing the following lines:

def getAccountInfo(clientId: Int, accountId: Int) = ???

and then running the risk of calling the function with accountId as the first param instead of the second (code would still compile)

We could use the refined types and transform it into something that is type-safe:

  case class ClientId(val clientId: Int) extends AnyVal
  case class AccountId(val accountId: Int) extends AnyVal

  def getAccountInfo(clientId: ClientId, accountId: AccountId) = ???

The second implementation would cause a compile time error if we try to call the function with the params inverted all while still giving us the same runtime performance.

Clearly Value Classes are great… but whats the catch

from the scala documentation: Because the JVM does not support value classes, Scala sometimes needs to actually instantiate a value class.

There are 3 cases during which a value class is actually instantiated:

  • a value class is treated as another type.
  • a value class is assigned to an array.
  • doing runtime type tests, such as pattern matching.

The third case is rather simple, so lets look into the first two:

when a value class is treated as another type

  trait Person extends Any

  case class Student(val name: String) extends AnyVal with Person

  def getName(person: Person) = ???
  
  val student1= Student("Emily")
  
  getName(student1)

Because we are calling a function that takes a Person and we are then passing it a Student that extends Person, the value class will be instantiated.

when a value class is assigned to an array

val classroom = Array[Student](student1)

In that case, the array classroom, will actually contain the instances of Student and not just the underlying datatype String


Overall, value classes can prove to be very useful and their use could bring significant improvement to your code.