5

I have several classes all extends the same trait and all share mutual functionality that should change their state. However I was wondering if is there a better way to implement the same functionality.

e.g :

trait Breed
case object Pincher extends Breed
case object Haski extends Breed

trait Foox{
  def age: Int
  def addToAge(i: Int): Foox 
}

case class Dog(breed: Breed, age: Int) extends Foox
case class Person(name: String, age: Int) extends Foox

I want that addToAge will return the same object with the additional int, of course I can implement the same for each class, which contradicts DRY rule:

case class Dog(breed: Breed, age: Int) extends Foox{
  def addToAge(i: Int) = copy(age = age + i)
}
case class Person(name: String, age: Int) extends Foox{
  def addToAge(i:Int) = copy(age = age + i)
}
  1. is there a better way to avoid that ?

  2. is there an option to avoid redefine that age:Int in each case class and maintain it's state (the age is already defined in the trait) ?

Kevin Wright
  • 49,540
  • 9
  • 105
  • 155
igx
  • 4,101
  • 11
  • 43
  • 88
  • Alright I am really curious about this. This question seems somewhat similar to (but not a duplicate of) [this](http://stackoverflow.com/questions/7227641/scala-how-can-i-make-my-immutable-classes-easier-to-subclass) and [this](http://stackoverflow.com/questions/21560470/method-inheritance-in-immutable-classes). Does anyone know if the answers given there help at all with this question? – evan.oman Jun 11 '16 at 20:54
  • You may be violating the immutability principle if you try to change the same object's state. Therefore `copy` seems like approach. The state should instead be maintained by the code that is using these objects i.e. update its own state to use the object returned by `addToAge` – tuxdna Jun 11 '16 at 21:08
  • I don't think there is an easy answer here. The closest think that comes to my mind is using some form of lenses or use the shapeless library. You have to sort of abandon OO and work with a more functional abstractions. For example, if you had a `map()` that just transforms the age and leaves everything else as is you would achieve what you need in this case. – marios Jun 11 '16 at 21:23
  • You could try `copy` method in trait as in this question https://stackoverflow.com/questions/5341120/rely-on-methods-of-case-class-in-trait, but that will only complicate and restrict your case class types. The reason is that `copy` is only available in the case class itself. – tuxdna Jun 11 '16 at 21:28
  • @tuxdna I do not intend to violate mutability , I just to avoid writing the exact same code – igx Jun 11 '16 at 21:50
  • 1
    Interesting question... seems like the magic around the "copy" method breaks the usual solutions (i.e structural typing and macros). You may be able to implement this using a lot of reflection code, but that's probably not what you're looking for. – Michael Bar-Sinai Jun 11 '16 at 22:25

1 Answers1

5

One possible solution, that may cover some use cases, is to use Lenses from the shapeless library:

import shapeless._

abstract class Foox[T](
  implicit l: MkFieldLens.Aux[T, Witness.`'age`.T, Int]
) {
  self: T =>
  final private val ageLens = lens[T] >> 'age

  def age: Int
  def addToAge(i: Int): T = ageLens.modify(self)(_ + i)
}

case class Dog(breed: Breed, age: Int) extends Foox[Dog]
case class Person(name: String, age: Int) extends Foox[Person]

Note that to create a Lens you need an implicit MkFieldLens, so it's easier to define Foox as an abstract class instead of a trait. Otherwise you'd have to write some code in every child to provide that implicit.

Also, I don't think there is a way to avoid defining an age: Int in every child. You have to provide the age in some way when you construct an instance, e.g. Dog(Pincher, 5), so you have to have that constructor argument for age there.


Some more explanation:

Borrowing from a Haskell Lens tutorial:

A lens is a first-class reference to a subpart of some data type. [...] Given a lens there are essentially three things you might want to do

  1. View the subpart
  2. Modify the whole by changing the subpart
  3. Combine this lens with another lens to look even deeper

The first and the second give rise to the idea that lenses are getters and setters like you might have on an object.

The modification part can be used to implement what we want to do with age.

Shapeless library provides a pretty, boilerplate-free syntax to define and use lenses for case class fields. The code example in the documentation is self explanatory, I believe.

The following code for the age field follows from that example:

final private val ageLens = lens[???] >> 'age
def age: Int
def addToAge(i: Int): ??? = ageLens.modify(self)(_ + i)

What should the return type of addToAge be? It should be the exact type of the subclass from which this method is being called. This is usually achieved with F-bounded polymorphism. So we have the following:

trait Foox[T] { self: T => // variation of F-bounded polymorphism

  final private val ageLens = lens[T] >> 'age

  def age: Int
  def addToAge(i: Int): T = ageLens.modify(self)(_ + i)
}

T is used there as the exact type of the child, and every class extending Foox[T] should provide itself as T (because of the self-type declaration self: T =>). For example:

case class Dog(/* ... */) extends Foox[Dog]

Now we need to make that lens[T] >> 'age line work.

Let's analyze the signature of the >> method to see what it needs to function:

def >>(k: Witness)(implicit mkLens: MkFieldLens[A, k.T]): Lens[S, mkLens.Elem]
  1. We see that the 'age argument gets implicitly converted to a shapeless.Witness. Witness represents the exact type of a specific value, or in other words a type-level value. Two different literals, e.g. Symbols 'age and 'foo, have different witnesses and thus their types can be distinguished.

    Shapeless provides a fancy backtick syntax to get a Witness of some value. For 'age symbol:

    Witness.`'age`    // Witness object
    Witness.`'age`.T  // Specific type of the 'age symbol
    
  2. Following from item 1 and the >> signature, we need to have an implicit MkFieldLens available, for class T (the child case class) and field 'age:

    MkFieldLens[T, Witness.`'age`.T]
    

    The age field should also have the type Int. It is possible to express this requirement with the Aux pattern common in shapeless:

    MkFieldLens.Aux[T, Witness.`'age`.T, Int]
    

And to provide this implicit more naturally, as an implicit argument, we have to use an abstract class instead of a trait.

Community
  • 1
  • 1
Kolmar
  • 14,086
  • 1
  • 22
  • 25
  • That was similar to what I had in mind. Can we add the imports as well just to make easier for someone to reproduce this in a repl? – marios Jun 12 '16 at 17:28
  • 1
    @marios OK, added. `shapeless._` is enough for it to work. Aditionally it needs the `Breed` definition from the original question. – Kolmar Jun 12 '16 at 19:40
  • @Kolmar , thanks that is exactly what I was looking for, can you please elaborate some more regarding this voodoo :) ? – igx Jun 14 '16 at 03:05
  • 1
    @igx Added some explanation – Kolmar Jun 14 '16 at 11:02