Diego Martinoia
Diego Martinoia

Reputation: 4662

Idiomatic approach to Scala Value Classes

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

Answers (2)

pyrospade
pyrospade

Reputation: 8078

I generally like to use the following pattern -

final class ProductId(val underlying: String) extends AnyVal

This has the following benefits -

  • You can access the underlying value whenever you need
  • Prevent pattern matching on the constructor by not using a case class, which helps to avoid actually constructing the object so the runtime value stays as a String (or whatever your underlying value is)
  • You can create a smart constructor using a companion object to validate its input and provide a cleaner interface when constructing the value so the user doesn't have to use 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

som-snytt
som-snytt

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

Related Questions