简体   繁体   中英

scala - idiomatic way to change state of class

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.

eg :

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) ?

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, eg 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, eg 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 
  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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM