Как в целом обновить поле класса дела с помощью LabelledGeneric?

Используя shapeless, можно использовать LabelledGeneric для обновления полей класса case следующим образом:

case class Test(id: Option[Long], name: String)
val test = Test(None, "Name")
val gen = LabelledGeneric[Test]

scala> gen.from(gen.to(test) + ('id ->> Option(1L)))
res0: Test = Test(Some(1),Name)

Я хотел бы, чтобы класс Test (и другие) расширил абстрактный класс Model, который будет реализовывать метод withId, который будет использовать LabelledGeneric, аналогичный приведенному выше коду, для обновления поля id, если оно есть (а должно).

Моя попытка добавляет неявный параметр LabelledGeneric[A] в конструктор Model, который прекрасно материализуется. Мне также нужно каким-то образом подтвердить синтаксис записи, что LabelledGeneric#Repr имеет поле id, которое нужно заменить. Добавление неявного параметра Updater в withId удовлетворяет компилятор, поэтому приведенный ниже код будет компилироваться, но его нельзя использовать.

import shapeless._, record._, ops.record._, labelled._, syntax.singleton._, tag._

abstract class Model[A](implicit gen: LabelledGeneric[A] { type Repr <: HList }) { this: A =>

    def id: Option[Long]

    val idWitness = Witness("id")

    type F = FieldType[Symbol with Tagged[idWitness.T], Option[Long]]

    def withId(id: Long)(implicit u: Updater.Aux[gen.Repr, F, gen.Repr]) =
        gen.from(gen.to(this) + ('id ->> Option(id)))

}

case class Test(id: Option[Long], name: String) extends Model[Test]

При вызове test.withId(1) неявное Updater не может материализоваться. Макрос сообщает, что gen.Repr не относится к типу HList, хотя на самом деле это так. Похоже, что

person Michael Zajac    schedule 02.03.2016    source источник


Ответы (1)


Основная проблема здесь в том, что уточненный тип результата LabelledGeneric (Repr) теряется. На Model о Repr известно только Repr <: HList. Неявный Updater.Aux[gen.Repr, F, gen.Repr] ищет то, что известно только как _ <: HList, и поэтому не может материализоваться. Вам нужно будет определить Model с двумя параметрами типа abstract class Model[A, L <: HList](implicit gen: LabelledGeneric.Aux[A, L]), но это не позволяет вам писать class Test extends Model[Test], и вам придется писать помеченный универсальный тип вручную.

Если вместо этого вы переместите gen вниз на withId, вы можете заставить его работать:

object Model {
  private type IdField = Symbol with Tagged[Witness.`"id"`.T]
  private val  IdField = field[IdField]

  type F = FieldType[IdField, Option[Long]]
}
abstract class Model[A] { this: A =>
  import Model._

  def id: Option[Long]

  def withId[L <: HList](id: Long)(implicit   // L captures the fully refined `Repr`
    gen: LabelledGeneric.Aux[A, L],           // <- in here ^
    upd: Updater.Aux[L, F, L]                 // And can be used for the Updater
  ): A = {
    val idf = IdField(Option(id))
    gen.from(upd(gen.to(this), idf))
  }
}

case class Test(id: Option[Long], name: String) extends Model[Test]

Если вас беспокоит производительность разрешения, вы можете кэшировать значения в сопутствующем Test:

case class Test(id: Option[Long], name: String) extends Model[Test]
object Test {
  implicit val gen = LabelledGeneric[Test]
}

Это будет означать, что такой код

val test = Test(None, "Name")
println("test.withId(12) = " + test.withId(12))
println("test.withId(12).withId(42) = " + test.withId(12).withId(42))

будет использовать определение Test.gen вместо того, чтобы каждый раз материализовать новый LabelledGeneric.

Это работает как для shapeless 2.2.x, так и для 2.3.x.

person knutwalker    schedule 12.04.2016