使用无形来合并同一案例类的两个实例

时间:2016-06-23 00:21:08

标签: scala shapeless

我想将类的更新实例合并到基本实例中,如果基本实例中的该字段为“空”,则选择基础实例上的更新实例的字段。以下示例合并baseupdate

case class Foo(a: Option[Int], b: List[Int], c: Option[Int])

val base = Foo(None, Nil, Some(0))
val update = Foo(Some(3), List(4), None)

merge(base,update) == Foo(Some(3), List(4), Some(0))

我尝试过这样的事情:

val g = Generic[Foo]
val aHList = g.to(base)
val bHList = g.to(update)

aHList
  .zipWithIndex
  .map({
    case (l: List, i: Int) => l ++ bHList(i)
    case (o: Option, i: Int) => if (!bHList(i).nonEmpty) {
      updateHList(i)
    } else {
      o
    }
    case (_, i: Int) => updateHList(i)
  })

但事实证明,通用.to方法不会输出HList,而是Repr。知道如何实现我的目标吗?

谢谢!

2 个答案:

答案 0 :(得分:5)

对于像这样的特定任务,创建类型类通常比使用map Poly之类的东西更容易。

我们可以代表某些T的更新,如下所示:

trait Update[T] {
  def apply(base: T, update: T): T
}

现在我们需要定义一些实例。如何更新ListOption以及某些实例,以便可以派生Update[Foo]个实例。

import shapeless._

object Update extends Update0 {
  def apply[A](implicit update: Lazy[Update[A]]): Update[A] = update.value

  implicit def optionUpdate[A]: Update[Option[A]] = 
    new Update[Option[A]] {
      def apply(base: Option[A], update: Option[A]): Option[A] = update orElse base
    }

  implicit def listUpdate[A]: Update[List[A]] = 
    new Update[List[A]] {
      def apply(base: List[A], update: List[A]): List[A] = base ++ update
    }

  implicit def hnilUpdate: Update[HNil] = 
    new Update[HNil] {
      def apply(base: HNil, update: HNil): HNil = HNil
    }

  implicit def hconsUpdate[H, T <: HList](
    implicit updateH: Update[H], updateT: Lazy[Update[T]]
  ): Update[H :: T] = 
    new Update[H :: T] {
      def apply(base: H :: T, update: H :: T): H :: T = 
        updateH(base.head, update.head) :: updateT.value(base.tail, update.tail)
    }
}

trait Update0 {
  implicit def genericUpdate[A, G <: HList](
    implicit gen: Generic.Aux[A, G], updateG: Lazy[Update[G]]
  ): Update[A] =
    new Update[A] {
      def apply(base: A, update: A): A = 
        gen.from(updateG.value(gen.to(base), gen.to(update)))
    }
}

我们可以添加一些语法以使其更容易一些:

implicit class UpdateOps[A](val base: A) extends AnyVal {
  def update(change: A)(implicit update: Lazy[Update[A]]): A = 
    update.value(base, change)
}

现在我们可以做到:

case class Foo(a: Option[Int], b: List[Int], c: Option[Int])

val base = Foo(None, Nil, Some(0))
val update = Foo(Some(3), List(4), None)

base update update // Foo(Some(3),List(4),Some(0))

我们可以为cats.SemigroupKscalaz.Plus定义一个实例,这样我们可以省略OptionList个实例,同时获得例如Update[Vector[Int]]

import cats.SemigroupK
import cats.implicits._

implicit def semigroupKUpdate[F[_], A](implicit F: SemigroupK[F]): Update[F[A]] =
  new Update[F[A]] {
    def apply(base: F[A], update: F[A]): F[A] = F.combineK(update, base)
  }

答案 1 :(得分:2)

您可以使用Polymap以非常一般的方式解决此问题。我认为解决方案非常优雅。

import shapeless._
import shapeless.ops.hlist._
import syntax.std.tuple._

trait LowPriority extends Poly1 {
  implicit def default[T, U] = at[(T, U)]{ case (t, u) => t }
}

object ChooseNonEmpty extends LowPriority {
  type OverNone[A] = (None.type, A)
  type OverNil[A] = (Nil.type, A)
  implicit def caseNone[T] = at[OverNone[T]] { case (t, u) => u }
  implicit def caseList[T] = at[OverNil[T]] { case (t, u) => u }
}

object test {
  def merge[C, HF <: Poly, Repr <: HList, ZRepr <: HList, MRepr <: HList](f:HF)(base:C, update:C)
  (implicit
       gen:Generic.Aux[C, Repr],
       zipper:Zip.Aux[Repr :: Repr :: HNil, ZRepr],
       mapper:Mapper.Aux[f.type, ZRepr, MRepr]): C = {

    val basep = gen.to(base)
    val updatep = gen.to(update)

    val zipped = basep zip updatep

    gen.from( (zipped map f).asInstanceOf[Repr])
  }
}

编辑:弄清楚如何使用Poly匹配元组中嵌入的None和Nil对象类型。添加它并简化代码。