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)
}
is there a better way to avoid that ?
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) ?
One possible solution, that may cover some use cases, is to use Lens
es 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
- View the subpart
- Modify the whole by changing the subpart
- 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]
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. Symbol
s '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
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With