6

I am trying to gather the fields of a case class that have a particular annotations at compile time using shapeless. I tried to play around the following snippet, but it did not work as expected (output nothing instead of printing "i"). How can I make it work ?

import shapeless._
import shapeless.labelled._

final class searchable() extends scala.annotation.StaticAnnotation
final case class Foo(@searchable i: Int, s: String)

trait Boo[A] {
  def print(a: A): Unit
}
sealed trait Boo0 {
  implicit def hnil = new Boo[HNil] { def print(hnil: HNil): Unit = () }
  implicit def hlist[K <: Symbol, V, RL <: HList](implicit b: Boo[RL]): Boo[FieldType[K, V] :: RL] =
    new Boo[FieldType[K, V] :: RL] {
      def print(a: FieldType[K, V] :: RL): Unit = {
        b.print(a.tail)
      }
    }
}
sealed trait Boo1 extends Boo0 {
  implicit def hlist1[K <: Symbol, V, RL <: HList](implicit annot: Annotation[searchable, K], witness: Witness.Aux[K], b: Boo[RL]): Boo[FieldType[K, V] :: RL] =
    new Boo[FieldType[K, V] :: RL] {
      def print(a: FieldType[K, V] :: RL): Unit = {
        Console.println(witness.value.name)
        b.print(a.tail)
      }
    }
}
object Boo extends Boo1 {
  implicit def generics[A, HL <: HList](implicit iso: LabelledGeneric.Aux[A, HL], boo: Boo[HL]): Boo[A] =
    new Boo[A] {
      def print(a: A): Unit = {
        boo.print(iso.to(a))
      }
    }
}

implicitly[Boo[Foo]].print(Foo(1, "2"))
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
Sheng
  • 1,697
  • 4
  • 19
  • 33

1 Answers1

3

Looking at the macro of Annotation, it rejects type that is not a product or coproduct straight up

val annTreeOpts =
  if (isProduct(tpe)) { ... }
  else if (isCoproduct(tpe)) { ... }
  else abort(s"$tpe is not case class like or the root of a sealed family of types")

this is quite unfortunate, as collecting type annotations at per field symbol level could be quite useful sometimes.

There is another type class Annotations defined in the same file that can actually collect particular annotations on field into an HList. However problem is the field information is totally lost. There is a clumsy way to hack things together to serve my use case...

// A is our annotation
// B is our result type    
// C is our case class with some fields annotated with A

def empty: B = ???
def concat(b1: B, b2: B): B = ???
def func(a: A, nm: String): B = ???

object Collector extends Poly2 {
   implicit def some[K <: Symbol](implicit witness: Witness.Aux[K]) = 
     at[B, (K, Some[A])] { case (b, (_, a)) => concat(b, func(a.get, witness.value.name)) }
   implicit def none[K <: Symbol] = at[B, (K, None.type)] { case (b, _) => b }
}

def collect[HL <: HList, RL <: HList, KL <: HList, ZL <: HList](implicit 
    iso: LabelledGeneric.Aux[C, HL]
  , annot: Annotations.Aux[A, C, RL]
  , keys: Keys.Aux[HL, KL]
  , zip: Zip.Aux[KL :: RL :: HNil, ZL]
  , leftFolder: LeftFolder.Aux[ZL, B, Collector.type, B]): B = {
  zip(keys() :: annot() :: HNil).foldLeft(empty)(Collector)
}
Sheng
  • 1,697
  • 4
  • 19
  • 33
  • Where is `Doc` coming from? – Yuval Itzchakov Jan 13 '18 at 14:20
  • Only product or product-like annotations are supported, so that these can be instantiated by shapeless at runtime (like `shapeless.Generic.from` does), and obtained via the `apply` methods of the `Annotation` and `Annotations` type classes. – Alex Archambault Jan 13 '18 at 14:32
  • @al3xar Problem is `Annotations` loses the field information, i.e. if one of the annotations contains an inner value, you don't have it, which is usually what you need when wrapping a type with a custom anno. – Yuval Itzchakov Jan 13 '18 at 14:35
  • That said, that's not the culprit here, your `searchable` has a `shapeless.Generic` instance. – Alex Archambault Jan 13 '18 at 14:37
  • @YuvalItzchakov `Annotations` has an apply method, that provides runtime instances of the annotations. – Alex Archambault Jan 13 '18 at 14:38
  • @al3xar But lets say you want to use these annotations to derive a case class codec at compile time. Can that be done? – Yuval Itzchakov Jan 13 '18 at 14:40
  • How do you use `collect`? – Yuval Itzchakov Jan 13 '18 at 16:34
  • lets say there is a case class, I can say I only want to collect and turn the annotated case class fields into a vector of Lucene fields. B can be Vector[Field], which already has an empty value. func could be reading the name of the field, and depending on the type of the field value, we can turn them into intfield, longfield, textfield etc – Sheng Jan 13 '18 at 16:51
  • @al3xar however what if I want to derive the annotation of a particular field with key type K at compile time to Some[searchable] or None.type ? – Sheng Jan 13 '18 at 16:53
  • @Sheng I see, but could you show a practical example using a case class? I've been trying to do something similar lately without success. – Yuval Itzchakov Jan 13 '18 at 17:14
  • @YuvalItzchakov I created a gist: https://gist.github.com/shengc/2b4052ed59419e86f36f48da35812634, it should be easy enough to generalize the idea to any case class. Should field level shapeless annotation type class be fixed, the programming interface would be much easier to understand and maintain. – Sheng Jan 14 '18 at 05:00
  • @Sheng Thanks for the gist! The thing I have difficulty grasping is who provides all the type parameters to `document` when invoking it. I assume they might be just infered by the compiler when providing an instance of `Person` (I'm refering to the `HL`, `AL`, etc type params). – Yuval Itzchakov Jan 14 '18 at 05:21
  • @Sheng Thanks a lot for all the help, Sheng. – Yuval Itzchakov Jan 15 '18 at 05:40