0
votes

I am trying to generate a typeclass for returning the value of a case class field that has a particular annotation at compile time, using shapeless.

That's given a Scala annotation case class and a generic case class the typeclass Identity[T] should return the value of the 'single' attribute annotated with such an annotation.

trait Identity[T] {
  def apply(t: T): Long
}

final case class Id() extends scala.annotation.StaticAnnotation
case class User(@Id id: Long, account_id: Int, name: String, updated_at: java.time.Instant)

and given an instance of the case class and typeclass

val identity   = Identity[User] // implicit summoner

val user = User(1009, 101, "Alessandro", Instant.now())
val value = identity(user) 

I'd like value to return 1009

I tried to play around the following snippet, but I did get as far as to compute the Symbol name of the annotated field.

object WithShapeless {

  import MyAnnotations.Id
  import shapeless._
  import shapeless.ops.hlist
  import shapeless.ops.record.Keys
  import shapeless.record._

  // select the field symbol with the desired annotation, if exists
  object Mapper extends Poly1 {
    implicit def some[K <: Symbol]: Case.Aux[(K, Some[Id]), Option[K]] = at[(K, Some[Id])] {
      case (k, _) => Some(k)
    }
    implicit def none[K <: Symbol]: Case.Aux[(K, None.type), Option[K]] = at[(K, None.type)] {
      case (k, _) => Option.empty[K]
    }
  }

  implicit def gen[A, HL <: HList, AL <: HList, KL <: HList, ZL <: HList, ML <: HList](
    implicit
    generic: LabelledGeneric.Aux[A, HL],
    annots: Annotations.Aux[Id, A, AL],
    keys: Keys.Aux[HL, KL],
    zip: hlist.Zip.Aux[KL :: AL :: HNil, ZL],
    mapper: hlist.Mapper.Aux[Mapper.type, ZL, ML],
    ev0: hlist.ToList[ML, Option[Symbol]]
  ): Identity[A] = new Identity[A] {

    val zipped: ZL          = zip(keys() :: annots() :: HNil)
    val annotatedFields: ML = mapper.apply(zipped)

    val symbol: Symbol = annotatedFields.to[List].find(_.isDefined).get match {
      case Some(symbol) => symbol
      case _            => throw new Exception(s"Class  has no attribute marked with @IdAnnot")
    }

    println(s"""
               |zipped: ${zipped}
               |mapped: ${annotatedFields}
               |symbol: $symbol
               |""".stripMargin)

    override def apply(a: A): Long = {
      val repr = generic.to(a)
      val value = repr.get(Witness(symbol)) // compilation fails here

      println(s"""
                 |Generic ${generic.to(a)}
                 |value: $value
      """.stripMargin)
      1
    }
  }
}

I try to conjure a Selector to return the value but the compiler fails with No field this.symbol.type in record HL.

I cannot get it to work! Thanks

1

1 Answers

0
votes

Actually you don't need LabelledGeneric because you don't use keys. Try

import java.time.Instant
import shapeless.ops.hlist.{CollectFirst, Zip}
import shapeless.{::, Annotations, Generic, HList, HNil, Poly1}

trait Identity[T] {
  type Out
  def apply(t: T): Out
}
object Identity {
  type Aux[T, Out0] = Identity[T] { type Out = Out0 }
  def instance[T, Out0](f: T => Out0): Aux[T, Out0] = new Identity[T] {
    type Out = Out0
    override def apply(t: T): Out = f(t)
  }

  def apply[T](implicit identity: Identity[T]): Aux[T, identity.Out] = identity

  implicit def mkIdentity[T, HL <: HList, AL <: HList, ZL <: HList](implicit
    generic: Generic.Aux[T, HL],
    annotations: Annotations.Aux[Id, T, AL],
    zip: Zip.Aux[HL :: AL :: HNil, ZL],
    collectFirst: CollectFirst[ZL, Mapper.type],
  ): Aux[T, collectFirst.Out] = 
    instance(t => collectFirst(zip(generic.to(t) :: annotations() :: HNil)))
}

object Mapper extends Poly1 {
  implicit def cse[A]: Case.Aux[(A, Some[Id]), A] = at(_._1)
}

final case class Id() extends scala.annotation.StaticAnnotation
case class User(@Id id: Long, account_id: Int, name: String, updated_at: java.time.Instant)

val identity = Identity[User]

val user = User(1009, 101, "Alessandro", Instant.now())
val value = identity(user) //  1009