如何使用Shapeless中的通用表示来修改任意案例类(例如,替换第一个字段)?

时间:2018-07-31 21:36:35

标签: scala shapeless

我正在尝试编写一个函数,该函数使用Shapeless将给定的任意case类用给定的新值替换第一个字段的值。

到目前为止,我有以下内容:

  def replaceHead[T, H, R >: H :: HList](obj: T, newHead: H)(implicit generic: Generic.Aux[T, R]): T =
    generic.to(obj) match {
      case h :: t => generic.from(newHead :: t)
    }

如果我不对R进行任何类型限制,则不能使用generic.from(newHead :: t),因为可以理解,它需要R的子类型。如果输入R >: H :: HList,则会出现以下错误:

Error:(19, 22) could not find implicit value for parameter generic: shapeless.Generic.Aux[Test.Record,String :: shapeless.HList]
  println(replaceHead(Record("abc", 123), "def"))

我要使其“工作”的唯一方法是使用这种类型安全的丑陋骇客:

  def replaceHead[T, H, R](obj: T, newHead: H)(implicit generic: Generic.Aux[T, R]): T =
    generic.to(obj) match {
      case h :: t => generic.from((newHead :: t).asInstanceOf[R])
    }

  replaceHead(Record("abc", 123), "def") // Works
  replaceHead(Record("abc", 123), 456) // Crashes at runtime

我知道根本的问题是因为R最终是例如String :: Int :: HNil,而 String :: HListString :: HList不一定{ {1}},但是在不删除尾部的类型签名的情况下,我无法找到一种访问通用表示形式的头的方法。

2 个答案:

答案 0 :(得分:1)

以下应该可以解决问题,

def replaceHead[C, R <: HList, H, T <: HList](c: C, newHead: H)
  (implicit
     gen: Generic.Aux[C, R],
     ev1: (H :: T) =:= R,
     ev2: R =:= (H :: T)
  ): C = gen.from(newHead :: gen.to(c).tail)

scala> replaceHead(Foo(23, "foo"), 13)
res0: Foo = Foo(13,foo)

我们需要两个类型相等性,以证明表示类型R在两个方向上都与H :: T相同。

答案 1 :(得分:0)

这是一个很好的问题,我不敢相信已经找到了最简单的解决方案。但这确实可行:

trait NonEmptyHList[T, Head] {
  def replaceHead(t: T, h: Head): T
}

implicit def nonEmptyHList[H, Rest <: HList]: NonEmptyHList[H :: Rest, H] = new NonEmptyHList[H :: Rest, H] {
  def replaceHead(t: H :: Rest, h: H): H :: Rest = t match {
    case _ :: rest => h :: rest
  }
}

def replaceHead[T, H, R](obj: T, h: H)(implicit generic: Generic.Aux[T, R], nonEmpty: NonEmptyHList[R, H]): T = {
  generic.from(nonEmpty.replaceHead(generic.to(obj), h))
}

val rec = new Record("hello", 1)
replaceHead(rec, "hi")  // Record("hi", 1)
replaceHead(rec, 2)  // compile error