6

Let's assume that we have a common trait Model.

trait Model {
  def id: String
  def updated: Date
}

And we have 2 case classes extending this trait.

case class C1(id: String, updated: Date, foo: String) extends Model
case class C2(id: String, updated: Date, bar: Int) extends Model

Is it possible to write a utility function like below which takes a Model as parameter and returns a copy with an updated value for the updated field?

object Model {
    def update[T <: Model](model: T): T = {
        model.copy(updated = new Date) // This code does not compile.
    }
}
fcs
  • 926
  • 1
  • 8
  • 14
  • 1
    [No](http://stackoverflow.com/q/12370244/298389) – om-nom-nom Apr 21 '14 at 13:28
  • 1
    @MilesSabin [I've seen that abstraction over arity was mentioned](http://stackoverflow.com/a/12492635/298389), but such overengineering is hardly a viable option (now, you can freely call me 'biased' or even a moron :-) ). Or there is some other way besides that? – om-nom-nom Apr 21 '14 at 13:54

3 Answers3

4

The "best" abstraction you can write here would be a Lens which looks like:

trait Lens[A, B]{
  def get: A => B
  def set: (A, B) => A
}

so that your code looks like:

def update[A](that: A, value: Date)(implicit tLens: Lens[A, Date]): A =
  tLens set (that, value)
wheaties
  • 35,646
  • 15
  • 94
  • 131
  • A lens is good ... but I think we can probably _infer_ the lens given a shapeless `LabelledGeneric` for the case classes in question. – Miles Sabin Apr 21 '14 at 18:41
  • 1
    @MilesSabin Yes, I bet you're right. If you write it, I'll bump that answer up. – wheaties Apr 21 '14 at 18:45
3

Your code has two issues:

  1. copy is not defined on the trait, so you need to have something defined on the trait that you can use.
  2. In order for update to return a T instead of a Model, every Model must know its actual subtype.

You can fix it like this:

trait Model[T <: Model[T]] {
  def id: String
  def updated: Date
  def withDate(d: Date): T
}

case class C1(id: String, updated: Date, foo: String) extends Model[C1] { def withDate(d: Date) = copy(updated = d) }
case class C2(id: String, updated: Date, bar: Int)    extends Model[C2] { def withDate(d: Date) = copy(updated = d) }

object Model {
  def update[T <: Model[T]](model: T): T = {
    model.withDate(new Date) // This code does not compile.
  }
}

So now it works:

scala> val c1 = C1("test", new Date, "foo")
c1: C1 = C1(test,Mon Apr 21 10:25:10 CDT 2014,foo)

scala> Model.update(c1)
res0: C1 = C1(test,Mon Apr 21 10:25:17 CDT 2014,foo)
dhg
  • 52,383
  • 8
  • 123
  • 144
  • This was the way I was planning to take. But I am still wondering if there is a way to do this automagically and type safe. – fcs Apr 22 '14 at 06:03
  • So even though `withDate` is the same line of code for every subtype of `Model` it needs to be rewritten for every class, correct? – Ritwik Bose Jul 07 '15 at 14:43
1

copy is a method defined on your case classes. Not on your base trait Model. What if you have that:

trait Model {
  def id: String
  def updated: Date
}

case class C1(id: String, updated: Date, foo: String) extends Model
case class C2(id: String, updated: Date, bar: Int) extends Model
class NotACaseClass(val id: String, val updated: Date) extends Model 

NotACaseClass is a very valid child of Model, and you could pass an instance of it to your update function, but good luck finding a copy method :)

vptheron
  • 7,426
  • 25
  • 34