Search code examples
scalaannotationsshapeless

Shapeless and annotations


I would like to have some function applied to fields in a case class, that are annotated with MyAnnotation. The idea is to transform type T into its generic representation, extract annotations, zip, fold right (or left) to reconstruct a generic representation and finally get back to type T. I followed the answer provided here and this gist.

I'm using scala 2.11.12 and shapeless 2.3.3.

Hereafter is my code:

import shapeless._
import shapeless.ops.hlist._

case class MyAnnotation(func: String) extends scala.annotation.StaticAnnotation

trait Modifier[T] {
  def modify(t: T): T
}

object Modifier {

  def apply[A: Modifier]: Modifier[A] = implicitly[Modifier[A]]

  def create[T](func: T => T): Modifier[T] = new Modifier[T] { override def modify(t: T): T = func(t) }

  private def id[T](t: T) = t

  implicit val stringModifier: Modifier[String] = create(id)

  implicit val booleanModifier: Modifier[Boolean] = create(id)

  implicit val byteModifier: Modifier[Byte] = create(id)

  implicit val charModifier: Modifier[Char] = create(id)

  implicit val doubleModifier: Modifier[Double] = create(id)

  implicit val floatModifier: Modifier[Float] = create(id)

  implicit val intModifier: Modifier[Int] = create(id)

  implicit val longModifier: Modifier[Long] = create(id)

  implicit val shortModifier: Modifier[Short] = create(id)

  implicit val hnilModifier: Modifier[HNil] = create(id)

  implicit def hlistModifier[H, T <: HList, AL <: HList](
    implicit
    hser: Lazy[Modifier[H]],
    tser: Modifier[T]
  ): Modifier[H :: T] = new Modifier[H :: T] {
    override def modify(ht: H :: T): H :: T = {
      ht match {
        case h :: t =>
          hser.value.modify(h) :: tser.modify(t)
      }
    }
  }

  implicit val cnilModifier: Modifier[CNil] = create(id)

  implicit def coproductModifier[L, R <: Coproduct](
    implicit
    lser: Lazy[Modifier[L]],
    rser: Modifier[R]
  ): Modifier[L :+: R] = new Modifier[L :+: R] {
    override def modify(t: L :+: R): L :+: R = t match {
      case Inl(l) => Inl(lser.value.modify(l))
      case Inr(r) => Inr(rser.modify(r))
    }
  }

  object Collector extends Poly2 {
    implicit def myCase[ACC <: HList, E] = at[(E, Option[MyAnnotation]), ACC] {
      case ((e, None), acc) => e :: acc
      case ((e, Some(MyAnnotation(func))), acc) => {
        println(func)
        e :: acc
      }
    }
  }

  implicit def genericModifier[T, HL <: HList, AL <: HList, ZL <: HList](
    implicit
    gen: Generic.Aux[T, HL],
    ser: Lazy[Modifier[HL]],
    annots: Annotations.Aux[MyAnnotation, T, AL],
    zip: Zip.Aux[HL :: AL :: HNil, ZL],
    rightFolder: RightFolder[ZL, HNil.type, Collector.type]
  ): Modifier[T] = new Modifier[T] {
    override def modify(t: T): T = {
      val generic = gen.to(t)
      println(generic)
      val annotations = annots()
      println(annotations)
      val zipped = zip(generic :: annotations :: HNil)
      println(zipped)
      val modified = zipped.foldRight(HNil)(Collector)
      println(modified)

      val typed = gen.from(generic) // temporary
      typed
    }
  }
}

The code above compiles. However, when instanciating a Modifier in a test:

  case class Test(a: String, @MyAnnotation("sha1") b: String)

  val test = Test("A", "B")
  val modifier: Modifier[Test] = implicitly

the test file does not compile and give the following error:

  [error] ambiguous implicit values:
  [error]  both value StringCanBuildFrom in object Predef of type => 
           scala.collection.generic.CanBuildFrom[String,Char,String]
  [error]  and method $conforms in object Predef of type [A]=> <:<[A,A]
  [error]  match expected type T
  [error]       val ser1: Modifier[Test] = implicitly

The problem seems to come from the right folder definition: when removing rightFolder from the list of implicits in genericModifier, then it works:

  implicit def genericModifier[T, HL <: HList, AL <: HList, ZL <: HList](
    implicit
    gen: Generic.Aux[T, HL],
    ser: Lazy[Modifier[HL]],
    annots: Annotations.Aux[MyAnnotation, T, AL],
    zip: Zip.Aux[HL :: AL :: HNil, ZL]/*,
    rightFolder: RightFolder[ZL, HNil.type, Collector.type]*/
  ): Modifier[T] = new Modifier[T] {
    override def modify(t: T): T = {
      val generic = gen.to(t)
      println(generic)
      val annotations = annots()
      println(annotations)
      val zipped = zip(generic :: annotations :: HNil)
      println(zipped)
      /*val modified = zipped.foldRight(HNil)(Collector)
      println(modified)*/

      val typed = gen.from(generic) // temporary
      typed
    }
  }

What is wrong?


Solution

  • There are several mistakes in your code:

    • defining Poly just for Option is too rough (pattern matching is performed at runtime and compiler should know definitions for Some and None at compile time)

    • HNil should be instead of HNil.type and HNil : HNil instead of HNil (types HNil and HNil.type are different)

    • compiler doesn't know that RightFolder actually returns the original HList type, so you should use RightFolder.Aux type.

    Correct code is

    import shapeless.ops.hlist.{RightFolder, Zip}
    import shapeless.{::, Annotations, Generic, HList, HNil, Lazy, Poly2}
    import scala.annotation.StaticAnnotation
    
    object App {
      case class MyAnnotation(func: String) extends StaticAnnotation
    
      object Collector extends Poly2 {
    //    implicit def myCase[ACC <: HList, E] = at[(E, Option[PII]), ACC] {
    //      case ((e, None), acc) => e :: acc
    //      case ((e, Some(MyAnnotation(func))), acc) => {
    //        println(func)
    //        e :: acc
    //      }
    //    }
    
        implicit def someCase[ACC <: HList, E]: Case.Aux[(E, Some[MyAnnotation]), ACC, E :: ACC] = at {
          case ((e, Some(MyAnnotation(func))), acc) =>
            println(func)
            e :: acc
        }
    
        implicit def noneCase[ACC <: HList, E]: Case.Aux[(E, None.type), ACC, E :: ACC] = at {
          case ((e, None), acc) => e :: acc
        }
      }
    
      trait Modifier[T] {
        def modify(t: T): T
      }
    
      implicit def hListModifier[HL <: HList]: Modifier[HL] = identity(_) 
      // added as an example, you should replace this with your Modifier for HList
    
      implicit def genericModifier[T, HL <: HList, AL <: HList, ZL <: HList](implicit
        gen: Generic.Aux[T, HL],
        ser: Lazy[Modifier[HL]],
        annots: Annotations.Aux[MyAnnotation, T, AL],
        zip: Zip.Aux[HL :: AL :: HNil, ZL],
        rightFolder: RightFolder.Aux[ZL, HNil/*.type*/, Collector.type, HL /*added*/]
        ): Modifier[T] = new Modifier[T] {
        override def modify(t: T): T = {
          val generic = gen.to(t)
          println(generic)
          val annotations = annots()
          println(annotations)
          val zipped = zip(generic :: annotations :: HNil)
          println(zipped)
          val modified = zipped.foldRight(HNil : HNil /*added*/)(Collector)
          println(modified)
    
          val typed = gen.from(modified)
          typed
        }
      }
    
      case class Test(a: String, @MyAnnotation("sha1") b: String)
    
      val test = Test("A", "B")
      val modifier: Modifier[Test] = implicitly[Modifier[Test]]
    
      def main(args: Array[String]): Unit = {
        val test1 = modifier.modify(test) // prints "sha1"
        println(test1) // Test(A,B)
      }
    }