Isaias Barroso
Isaias Barroso

Reputation: 83

Return a new modified field object from a scala function that the input parameter is based on a trait

I'm trying to model a trait/class structure to a generic Data Access Layer using Slick, but to limit just on fundamental part I'll post a basic code that have the general idea of the problem.

Suppose I have a base trait that define the common attributes for a Entity.

trait Base {
  def id: Option[Long]
  def version: Long
} 

Now I'll create the Entities based on that trait.

case class User(id: Option[Long], version: Long, name: String, age: Int) extends Base

And a function that will get the Base type to be generic. At that point I can create, update the objects on database without problem. My problem is when I want to return the original object with the generated id returned from database.

def insert[M <: Base](m: M): M = {
  //insert on database and get the new ID
  val newId = Some(10)
  // Now I want to return the object with the newId
  // As Base is a trait I don't have copy
  m
}

val user = User(None, 1, "Test", 34)
insert(user)

To illustrate I would like to get a new User with id = Some(10) as result for insert function.

I thought about use copy, but it will work if I declare the function with a Base case class instead a Trait, but isn't what I want initially. I tried to use Lens, like scalaz Lens. But I'll need copy too.

I'm missing something? There is another way to do it without using Reflection?

Thanks

Upvotes: 2

Views: 244

Answers (1)

Ben Reich
Ben Reich

Reputation: 16324

You could use F-bound polymorphism to require a withId method on the trait that is required to return the expected type:

trait Base[Self <: Base[Self]] { self: Self =>
  def id: Option[Long]
  def version: Long
  def withId(id: Long): Self
} 

You can then implement withId on any case class by calling its native copy method:

case class User(id: Option[Long], version: Long, name: String, age: Int) extends Base[User] {
  def withId(id: Long) = this.copy(id = Some(id))
}

Then you could define your insert method as:

def insert[M <: Base[M]](m: M): M = {
  m.withId(10)
}

And the rest should work as expected.

The real problem is that copy is a special compiler-generated method, and you cannot require its existence on a trait. This use of F-bound polymorphism allows you to work around this limitation with limited boilerplate.

Another approach is to just add another trait, say HasWithId that guarantees the withId method, and extend/require that wherever needed:

trait Base {
  def id: Option[Long]
  def version: Long
} 

trait HasWithId[M] {
  def withId(id: Long): M
}

case class User(id: Option[Long], version: Long, name: String, age: Int) extends Base with HasWithId[User] {
  def withId(id: Long): User = this.copy(id = Some(id))
}

def insert[M <: Base with HasWithId[M]](m: M) = {
  m.withId(10)
}

Upvotes: 3

Related Questions