Value classes in business domain

Let’s assume that we want to model a class ‘Person’ that holds a first name, the last name and date of birth.
Is it good practice to create value classes for the properties?

Example:

import java.time.LocalDate

import Person.{DateOfBirth, FirstName, LastName}

final case class Person(firstName: FirstName,
                        lastName: LastName,
                        dateOfBirth: DateOfBirth)

object Person {
  final case class FirstName(value: String) extends AnyVal
  final case class LastName(value: String) extends AnyVal
  final case class DateOfBirth(value: LocalDate) extends AnyVal
}

Or is this rather over-the-top?

1 Like

It’s a case-by-case decision…

Is there some constraint that excludes possible values from the native data type range (silly ex.: DoB should be CE and can never be > now)? Could a new abstraction be reused across the code base (other code than Person handling DoB values)? Are we wrapping a 3rd party type (historically: LocalDate in joda-time prior to java.time)? Each “yes” would push me closer to introducing a new type.

And there’s alternatives to native data types and dedicated (case) classes: Type aliases, tagged types, intermediate combined types (e.g. Name(first: String, last: String)),…

1 Like

Thank you for your response!

I always thought it was bad practice to have multiple parameters of the same type for a function, and therefore, I thought to create this value classes was a good idea.

This strikes me as a rather mechanistic view - what about plus(a: Int, b: Int), then? :wink:

But yes, avoiding ambiguity certainly is another touchstone - even with the Name abstraction suggested above, there’s still some risk of confusing the first and last name component when creating a Name instance or entering a code path processing the last name only, so one could still advocate introducing GivenName and FamilyName types on top - at the expense of having person.name.first.value across the code instead of the “weakly typed” person.firstName.

Of course the code and performance overhead for new types is a criterion, as well - I’d assume that dotty opaque types will lower this barrier.

There’s some sweet spot between introducing custom types and (re)using “given” types. I agree that the balance should be more geared towards introducing custom types. The exact tipping point depends on how easy/“natural” the language makes it - I think I’m using newtype in Haskell more freely than I’m introducing single-member case classes in Scala.

That being said, when in doubt, it probably won’t hurt to introduce a type alias to start with.

2 Likes

It is a good practice to add newtypes for improving the typesafety of a bussiness model.

Generally yes, but make sure it makes sense and the tradeoff pays off.
@sangamon already explained this point, so I won’t go further.

Are ValueClasses a good mechanism to model newtypes.

No they aren’t. They are very limited and unpredictable.
One may use them thinking they won’t box. But they may, and they may even box many times, which makes them worse than just simple wrappers.

1 Like

My opinion (we should be clear, this is a matter of taste) is generally yes – I usually wrap primitive types like this (which heads off a lot of bugs), and often use AnyVal. But as others have mentioned, AnyVal is erratic, and doesn’t always work as desired, so that bit isn’t obvious. Opaque types (coming in Scala 3) will handle this more correctly…

1 Like

I thought Value Classes are just a compile-time concept?

No – opaque types (yet to come) are strictly a compile-time concept. Current value types are messier: they mostly don’t exist at runtime, but wind up getting brought into existence as boxes surprisingly often.

1 Like

But I’m afraid my question was purely academic: I just realized that a parameter of the type of a value class must not be a default parameter, for whatever reason:

final case class Name(value: String) extends AnyVal

def f(name: Name = null) = ??? // type mismatch; found: Null(null) required: Playground.Name

In general, null is a really bad idea in Scala code, and very strongly discouraged. Is it necessary?

1 Like

How would you express non-mandatory parameters without massive overloading or the builder pattern?

Option – that’s what it is there for, and it is used ubiquitously in Scala. Get used to it: 99.9% of the time, where you would use null in Java, you should be using Option in Scala. The only exceptions are (a) absolutely performance-critical code, and (b) code that is directly interfacing with Java. (Which will typically receive null from Java and immediately convert it to Option.)

Raw nulls are quite rare in idiomatic Scala – in my group, they’re cause for immediate rejection of pull requests. They are consistently more trouble than they are worth. (The billion-dollar bug and all that.)

And yes, this does sometimes mean that you use overloads to avoid saying Some(thing) too much, although in practice I find that I don’t need to do it so much that it becomes a problem. I frequently find that, when there are a lot of optional parameters, they frequently turn out to be logically related such that there is more appropriately a single optional parameter of a sub-structure. And at other times I often find that there is actually a sensible non-empty default parameter.

(The one place I do encounter this a lot is deserialization, but that has its own collection of patterns – generally typeclass-based – for concisely expressing what you mean.)

2 Likes

The problem is not the default value, but it being null. Apart from null usage being a very bad practice in Scala, it can only be a value for reference types (AnyRef) while your class extends AnyVal.

1 Like

Are you sure? I have never seen an Option as a parameter type, and I am pretty sure that Option is not intended as a parameter type.

At least in Java it is discouraged to use Optional as input type.

I have seen it, though not very frequent - probably because (non-null!) default parameter values seem a better choice if applicable. More frequently I see case class members typed to Option, though.

To tilt the angle a bit: How should optional values be represented other than with Option, given that null is strictly discouraged in Scala? Actually it’s more than just discouraged, it only exists for Java interop:

Null is a source of many, many errors. We could have come out with a language that just disallows null as a possible value of any type, because Scala has a perfectly good replacement called an option type. But of course a lot of Java libraries return nulls and we have to treat it in some way.
artima - The Goals of Scala's Design

That’s correct, but it doesn’t affect the intended use cases of the Scala Option type (which has been around much longer than Java’s Optional).

The JSR-335 EG felt fairly strongly that Optional should not be on any more than needed to support the optional-return idiom only. (Someone suggested maybe even renaming it to OptionalReturn to beat users over the head with this design orientation; perhaps we should have taken that suggestion.) I get that lots of people want Optional to be something else.
Shouldn't Optional be Serializable?

This “something else” is Scala’s Option (or Haskell’s Maybe).

1 Like

If you have a parameter that is truly optional (not just that it has a sensible default that you want 90% of the time) I think it’s perfectly fine to use Option. Probably that won’t happen extremely often because usually when you write a function that does something with a name it actually needs a name to do something useful. I think that advice is probably geared towards cases like this:

val maybeName: Option[Name] = ???

// don't do this
def useName1(name: Option[Name]): Option[Result] = ???
useName1(maybeName)

// do this
def useName2(name: Name): Result = ???
maybeName.map(useName2)

That’s simply not true – it’s used as a parameter type in a good number of places, for situations where the value is really optional. It’s just that that situation isn’t all that common.

The big difference between Java’s attitude here and Scala’s is that Scala consistently forces you to think about optionality – to ask whether you actually want this to be optional, and to consistently handle it if you do. The result is, very intentionally, a little more ceremony in the cases where you do care.

As @sangamon alludes to, it’s more often the case that what you want isn’t a missing value (which is what Option allows for), but a default value of the desired type. That’s why you don’t see Option parameters really frequently – there’s often a better answer.

1 Like

Thank you all for your answers so far.

Let’s put a concrete example to the table: We want to model a Person class which can hold the following properties:

  • Given name
  • Surname(*)
  • Date of birth
  • Place of birth
  • Nationality(*)

Surname and Nationality are mandatory, everything else is not.

Variant 1:

final case class Person(
    surname: String,
    nationality: String,
    private val _givenName: String,
    private val _dateOfBirth: LocalDateTime,
    private val _placeOfBirth: String
) {

  val givenName: Option[String] = Option(_givenName)
  val dateOfBirth: Option[LocalDateTime] = Option(_dateOfBirth)
  val placeOfBirth: Option[String] = Opion(_placeOfBirth)
}

object Person {
  def apply(
      surname: String,
      nationality: String,
      givenName: String = null,
      dateOfBirth: LocalDateTime = null,
      placeOfBirth: String = null
  ): Person = {
    require(surname != null && nationality != null)
  }
}

Variant 2:

final case class Person(
    surname: String,
    nationality: String,
    givenName: Option[String] = None,
    dateOfBirth: Option[LocalDateTime] = None,
    placeOfBirth: Option[String] = None
) {
  require(surname != null && nationality != null)
}

If only the mandatory fields are supplied, the call looks same for both variants:

val p = Person("Doe", "AT")

If we were to supply the mandatory values plus the place of birth things are different:

val p1 = Person("Doe", "AT", placeOfBirth = "New York City")
val p2 = Person("Doe", "AT", placeOfBirth = Some("New York City"))

In my opinion, the notation of variant 2 can become quite cumbersome, especially if there is a large number of parameters which are non-mandatory.

Variant 1 may look like a successful attempt to encapsulate the nulls, but they will be leaking (e.g. through pattern matches), they will trigger NPEs in libraries that rely on reflection (e.g. JSON formatters), and so on. They will come back to haunt you, promise! :slight_smile:

Again: null is only a thing in Scala for reasons of Java interop. If you insist on using it otherwise, you’re working against the grain of the language.

I’d also suspect that the data for Person instances will usually rather come from sources like CSV, JSON, XML,… than from hardcoded literals - and there you’ll probably be dealing with APIs that can already provide you with Option instances for optional fields.

3 Likes

I am convinced, I will go with Option!

Thank you all again for your help :slight_smile: