Shapeless: извлечь значение поля case case, параметризованное аннотацией

Я пытаюсь создать класс типов для возврата значения поля класса case, которое имеет определенную аннотацию во время компиляции, используя shapeless.

Это дает класс случая аннотации Scala и общий класс случая, класс типов Identity[T] должен возвращать значение «одиночного» атрибута, аннотированного такой аннотацией.

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)

и учитывая экземпляр класса case и typeclass

val identity   = Identity[User] // implicit summoner

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

Я хочу, чтобы value вернул 1009

Я попытался поиграть со следующим фрагментом, но мне удалось вычислить Symbol имя аннотированного поля.

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
    }
  }
}

Я пытаюсь вызвать Selector, чтобы вернуть значение, но компилятор терпит неудачу с No field this.symbol.type in record HL.

Я не могу заставить его работать! Спасибо


person GrayMouser    schedule 12.03.2021    source источник


Ответы (1)


На самом деле вам не нужно LabelledGeneric, потому что вы не используете ключи. Пытаться

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
person Dmytro Mitin    schedule 14.03.2021
comment
Ваше решение вызывает все бесформенные методы в реализации apply. В моей (частичной) попытке я попытался максимально учесть (вот почему я использовал keys) код, не относящийся ко времени выполнения, за пределами метода apply с целью иметь облегченный метод apply, который я могу запускать большую коллекцию данных (т.е. список из миллионов элементов). Можно ли адаптировать ваше решение так, чтобы в методе Identity apply мы просто извлекали значение и сохраняли код бесформенной косички, чтобы жить в свойстве, а не в самом методе? Спасибо - person GrayMouser; 16.03.2021
comment
@GrayMouser Не уверен, что понял ваш вопрос. Вы можете реорганизовать код любым удобным для вас способом. Вам просто не нужно LabelledGeneric. Generic достаточно. - person Dmytro Mitin; 17.03.2021
comment
я имел в виду, что ваш код выполняет collectFirst(zip(generic.to(t) :: annotations() :: HNil)) при **каждом ** вызове метода apply. Эти операции довольно тяжелые (тест производительности на коллекции из 1M users занял более 5 секунд, а реализация, основанная на ручном создании класса типов, около 150 мс. Можно ли реорганизовать ваш код в ? : Aux[...] = new Identity[T] { ... val extractor = // here all heavy weight code def apply(t: T): Out = extractor(..) // use object above to extract the value } } Спасибо - person GrayMouser; 17.03.2021