FaNaJ
FaNaJ

Reputation: 1359

Scala - union types in pattern matching

I have a trait like this:

trait Identifiable {

  def id: Option[Long]

}

and then there are some other case classes which extend the Identifiable trait. for example:

case class EntityA(id: Option[Long], name: String, created: Date) extends Identifiable

case class EntityB(id: Option[Long], price: Long, count: Int) extends Identifiable

assume that I have a Seq[Identifiable] and I want to assign new id to each one.

the simplest approach seems to be:

val xs: Seq[Identifiable] = ...
xs.map {
  case x: EntityA => x.copy(id = Some(nextId))
  case x: EntityB => x.copy(id = Some(nextId))
}

good! but there's is a problem. The more subclasses, The more (duplicate) code to be written.

I tried to get help from Union Types:

xs.map {
  case x: EntityA with EntityB => x.copy(id = Some(nextId))
}

or

xs.map {
  case x @ (_: EntityA | _: EntityB) => x.copy(id = Some(nextId))
}

but I got an error that says: Cannot resolve symbol copy

Any help would be appreciated. Thanks.

Upvotes: 1

Views: 2429

Answers (2)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149646

Basically, what we want to do here is abstract over the actual type. The problem with that is copy is only implemented OOTB in terms of case classes, and Identifiable is a trait, so there may or may not be a copy method available at compile time, hence why the compiler is yelling at you.

Heavily inspired by this answer, I modified the provided example which uses Shapeless lenses:

import shapeless._

abstract class Identifiable[T](implicit l: MkFieldLens.Aux[T, Witness.`'id`.T, Option[Long]]){
  self: T =>
  final private val idLens = lens[T] >> 'id

  def id: Option[Long]
  def modifyId(): T = idLens.modify(self)(_ => Some(Random.nextLong()))
}

case class EntityA(id: Option[Long], name: String, create: Date) extends Identifiable[EntityA]
case class EntityB(id: Option[Long], price: Long, count: Int) extends Identifiable[EntityB]

And now, we can modify each id on any type extending Identifable[T] for free:

val xs: Seq[Identifiable[_]] = Seq(EntityA(Some(1), "", new Date(2017, 1, 1)), EntityB(Some(2L), 100L, 1))
val res = xs.map(_.modifyId())
res.foreach(println)

Yields:

EntityA(Some(-2485820339267038236),,Thu Feb 01 00:00:00 IST 3917)
EntityB(Some(2288888070116166731),100,1)

There is a great explanation regarding the individual parts assembling this answer in the provided link above by @Kolmar, so first and foremost go read the details of how lensing works for the other answer (which is very similar), and then come back to this for a reference of a minimal working example.

Also see @Jasper-M answer here for more ways of accomplishing the same.

Upvotes: 2

Matt Fowler
Matt Fowler

Reputation: 2743

Union types aren't the right path here. Consider:

xs.map {
  case x @ (_: EntityA | _: EntityB) => x.copy(id = Some(nextId))
}

When you say EntityA | EntityB Scala will try and find the supertype that holds these two types together. In this case that is Identifiable, which does not have the copy method and therefore the compiler can't resolve it.

Next:

xs.map {
  case x: EntityA with EntityB => x.copy(id = Some(nextId))
}

When you say EntityA with EntityB you're saying "x is a type that is both an EntityA and EntityB at the same time". No such type exists and certainly not one that has a copy method on it.

Unfortunately, I don't think you can generically abstract over the copy method the way you're looking to do in plain Scala. I think your best bet is to add a copy method to your trait and implement methods in each of your sub-classes like so, which unfortunately means some boilerplate:

trait Identifiable {
  def id: Option[Long]
  def copyWithNewId(newId: Option[Long]): Identifiable
}

case class EntityA(id: Option[Long], name: String) extends Identifiable {
  override def copyWithNewId(newId: Option[Long]) = this.copy(id = newId)
}

case class EntityB(id: Option[Long], count: Int) extends Identifiable {
  override def copyWithNewId(newId: Option[Long]) = this.copy(id = newId)
}

This is more or less with your working pattern matching, except moving the copy call into the entities themselves.

Now this only applies to plain Scala. You can used more advanced libraries, such as Shapeless or Monocle to do this. See this answer which is pretty similar to what you're trying to do:

Case to case inheritence in Scala

Upvotes: 2

Related Questions