Reputation: 4662
Does anyone know if there is an idiomatic way to name the inner value of a Scala Value Class? Say I have a value class for a product id, is it better defined as:
case class ProductId(productId:String) extends AnyVal
case class ProductId(underlying:String) extends AnyVal
case class ProductId(value:String) extends AnyVal
?
Is it just a matter of preference or is there an idiomatic guideline?
Upvotes: 2
Views: 238
Reputation: 8078
I generally like to use the following pattern -
final class ProductId(val underlying: String) extends AnyVal
This has the following benefits -
underlying
value whenever you needcase class
, which helps to avoid actually constructing the object so the runtime value stays as a String
(or whatever your underlying value is)new
.Example of a smart constructor below -
final class ProductId(val underlying: String) extends AnyVal
object ProductId {
def apply(s: String): Result = {
if (s.isEmpty) {
new Failure("ProductId cannot be empty!")
} else {
new Success(new ProductId(s))
}
}
sealed trait Result
final case class Success(productId: ProductId) extends Result
final case class Failure(message: String) extends Result
}
If you want to ensure that users must use the smart constructor, mark your value class' constructor as private -
final class ProductId private (val underlying: String) extends AnyVal
If you want to ensure that you're not accidentally allocating an instance of ProductId
, you can check the bytecode -
scala> :paste
class Test {
def testProductId = new ProductId("foo")
def testSmartCtor = ProductId("bar") match {
case ProductId.Success(productId) => productId
case ProductId.Failure(message) => throw new AssertionError(message)
}
}
// Ctrl+D
:javap -c ProductId$
// Skipping to the apply() method
public ProductId$Result apply(java.lang.String);
Code:
0: aload_1
1: invokevirtual #20 // Method java/lang/String.isEmpty:()Z
4: ifeq 19
7: new #22 // class ProductId$Failure
10: dup
11: ldc #24 // String ProductId cannot be empty!
13: invokespecial #27 // Method ProductId$Failure."<init>":(Ljava/lang/String;)V
16: goto 27
19: new #29 // class ProductId$Success
22: dup
23: aload_1
24: invokespecial #30 // Method ProductId$Success."<init>":(Ljava/lang/String;)V
27: areturn
There are no new ProductId
references in the bytecode, so at runtime your ProductId
will be represented as a String
.
Note that if you try to wrap a value class in a class which uses generics (e.g. Option, Either) then your value will get boxed. You can avoid this by creating a simple case class which is specialized for your value class. While the case class will get instantiated (since you can't wrap a value class with another value class) the underlying ProductId
will still be represented as String
at runtime.
Upvotes: 2
Reputation: 39577
The idiomatic approach is to make it a private val
, now that you can.
The standard library, so far as standards go, prefers self
.
implicit final class ArrowAssoc[A](private val self: A) extends AnyVal
There is also repr
, recalling both the phrase "underlying runtime representation" from the scaladoc for AnyVal
and the repr
of collections.
class StringOps(override val repr: String) extends AnyVal with StringLike[String]
There's also a smattering of i
and n
.
Personally, I use the special identifier, YMMV
.
Upvotes: 1