A value class in Scala is a special kind of class that aims to provide the benefits of custom data types without the overhead of additional runtime objects. Value classes can be used to define lightweight and performant classes that wrap a single value. They were introduced in Scala 2.10 as a means to avoid runtime instantiation and garbage collection costs, which can be significant for small objects that are created and discarded frequently.
Value classes differ from regular classes in two main ways:
1. They must extend ‘AnyVal‘ instead of ‘AnyRef‘ (the default base class for Scala classes).
2. They can have only a single, immutable parameter in their primary constructor.
To define a value class, you use the ‘extends AnyVal‘ clause in the class definition and add the ‘val‘ keyword in the primary constructor:
class Meter(val value: Double) extends AnyVal {
def +(other: Meter): Meter = new Meter(value + other.value)
}
Here, ‘Meter‘ is a value class that wraps a ‘Double‘. It has a single method, ‘+‘, to add two ‘Meter‘ instances. Because ‘Meter‘ is a value class, the addition operation will have minimal overhead compared to directly working with ‘Double‘ values.
Some important restrictions apply to value classes:
1. They cannot have their own ‘equals‘, ‘hashCode‘, or ‘toString‘ methods.
2. They can define only ‘def‘s, not fields or ‘val‘s.
3. They cannot be extended by other classes or value classes.
4. They cannot take type parameters (i.e., they cannot be generic).
The main advantage of value classes is that, under certain circumstances, their instances can be represented at runtime by the underlying values they wrap, which means no instantiation cost and no garbage collection. However, this optimization is not guaranteed, and in some cases instances of value classes may still be created at runtime. The Scala Language Specification provides a detailed list of conditions under which value classes can be "boxed" (i.e., represented as objects).
In contrast, regular classes in Scala, which extend ‘AnyRef‘ by default, always have runtime instantiation and garbage collection costs. If you have a small, simple, and immutable data type that you need to use frequently, you might consider using a value class for performance reasons.
It is also worth to mention Universal Traits. Universal traits are traits that can be extended by both ‘AnyVal‘ (value classes) and ‘AnyRef‘ (regular classes). To define a universal trait, you need to extend ‘Any‘ (the base class for all Scala types) and mark it with a ‘sealed‘ modifier.
sealed trait Measurement extends Any {
def value: Double
}
Now the value class can extend this universal trait:
class Meter(val value: Double) extends AnyVal with Measurement {
def +(other: Meter): Meter = new Meter(value + other.value)
}
Keep in mind that mixing in a universal trait only affects the compile-time representation of a value class and does not generally impact its runtime performance.
To summarize, value classes in Scala are a lightweight alternative to regular classes that can provide better performance characteristics for specific use cases of small, simple, and immutable data types. They have some restrictions and trade-offs compared to regular classes, but can be beneficial when used appropriately.