6
votes

I am trying to create a type class that will allow me to increment an Int field called "counter" on any case class, as long as that class has such a field.

I have tried to do this with Shapeless but am hitting walls (after first trying to digest "The Type Astronaut's Guide to Shapeless", the "Feature overview" of Shapeless 2.0.0 and numerous threads on Stack Overflow).

What I want is to be able to do something like

case class MyModel(name:String, counter:Int) {}

val instance = MyModel("Joe", 4)
val incremented = instance.increment()
assert(incremented == MyModel("Joe", 5))

And it should work for any case class with a suitable counter field.

I thought that this would be possible using a type class and Shapeless' record abstraction (and an implicit conversion to get the increment functionality added as a method). The bare bones would be something like this:

trait Incrementer[T] {
  def inc(t:T): T
}

object Incrementer {
  import shapeless._ ; import syntax.singleton._ ; import record._

  implicit def getIncrementer[T](implicit generator: LabelledGeneric[T]): Incrementer[T] = new Incrementer[T] {
    def inc(t:T) = {
      val repr = generator.to(t)
      generator.from(repr.replace('counter, repr.get('counter) + 1))
    }
  }     
}

However, this does not compile. The error being value replace is not a member of generator.Repr. I guess this is because the compiler does not have any guarantee as to T having a field called counter and it being of type Int. But how could I tell it so? Is there better/more documentation on Shapeless' record? Or is this a completely wrong way to go?

2

2 Answers

8
votes

You have to just implicitly require a Modifier

import shapeless._
import ops.record._
implicit class Incrementer[T, L <: HList](t: T)(
  implicit gen: LabelledGeneric.Aux[T, L],
  modifier: Modifier.Aux[L, Witness.`'counter`.T, Int, Int, L]
) {
  def increment(): T = gen.from(modifier(gen.to(t), _ + 1))
}
0
votes

You can easily do it with a simple typeclass derivation:

trait Incrementer[T] {
  def inc(s: Symbol)(t: T): T
}

object Incrementer {
  def apply[T](implicit T: Incrementer[T]): Incrementer[T] = T
  implicit def head[Key <: Symbol, Head, Tail <: HList](implicit Key: Witness.Aux[Key], Head: Numeric[Head]) = new Incrementer[FieldType[Key, Head] :: Tail] {
    import Head._
    override def inc(s: Symbol)(t: FieldType[Key, Head] :: Tail): (FieldType[Key, Head] :: Tail) =
      if (s == Key.value) (t.head + fromInt(1)).asInstanceOf[FieldType[Key, Head]] :: t.tail
      else t
  }

  implicit def notHead[H, Tail <: HList](implicit Tail: Incrementer[Tail]) = new Incrementer[H :: Tail] {
    override def inc(s: Symbol)(t: H :: Tail): H :: Tail = t.head :: Tail.inc(s)(t.tail)
  }

  implicit def gen[T, Repr](implicit gen: LabelledGeneric.Aux[T, Repr], Repr: Incrementer[Repr]) = new Incrementer[T] {
    override def inc(s: Symbol)(t: T): T = gen.from(Repr.inc(s)(gen.to(t)))
  }
}

case class Count(counter: Int)
case class CountAndMore(more: String, counter: Int)
case class FakeCount(counter: Long)
object Test extends App {

  println(Incrementer[Count].inc('counter)(Count(0)))
  println(Incrementer[CountAndMore].inc('counter)(CountAndMore("", 0)))
  println(Incrementer[FakeCount].inc('counter)(FakeCount(0)))
}