Как представить частичное обновление в case classe в Scala?

У меня есть следующий класс case:

case class PropositionContent(title:String,content:String)

И я хотел бы представить частичную модификацию его как Data.

Один из способов - создать класс case:

case class PartialPropositionContent(title:Option[String],content:Option[String)

а затем некоторые методы:

object PropositionContent {

   def append( pc  : PropositionContent
             , ppc : PartialPropositionContent) =
   PropositionContent ( ppc.title.getOrElse(pc.title)
                      , ppc.content.getOrElse(pc.content) )

   def append( ppc  : PartialPropositionContent
             , ppc2 : PartialPropositionContent ):  PartialPropositionContent = {...}

}

Но это немного котельная!

Я думаю, что case class PropositionContent[M[_]](title:M[String],content:M[String]) не решит проблему, и я не знаю, как использовать Shapeless для решения этой проблемы.

У вас есть идея?

Ответ 1

Здесь используется относительно безшаблонный подход с использованием Shapeless. Сначала мы определяем некоторые полиморфные версии соответствующих функций на Option:

import shapeless._

object orElser extends Poly1 {
  implicit def default[A] = at[Option[A]] {
    oa => (o: Option[A]) => oa orElse o
  }
}

object getOrElser extends Poly1 {
  implicit def default[A] = at[Option[A]] {
    oa => (a: A) => oa getOrElse a
  }
}

Мы представим обновление как HList, где каждый элемент является Option, и мы можем написать метод append, который позволяет нам добавить два обновления:

import UnaryTCConstraint._

def append[U <: HList: *->*[Option]#λ, F <: HList](u: U, v: U)(implicit
  mapper: MapperAux[orElser.type, U, F],
  zipper: ZipApplyAux[F, U, U]
): U = v.map(orElser).zipApply(u)

И, наконец, мы можем написать наш метод update:

def update[T, L <: HList, F <: HList, U <: HList](t: T, u: U)(implicit
  iso: Iso[T, L],
  mapped: MappedAux[L, Option, U],
  mapper: MapperAux[getOrElser.type, U, F],
  zipper: ZipApplyAux[F, L, L]
) = iso from u.map(getOrElser).zipApply(iso to t)

Теперь нам нужно только немного шаблона (в будущее это не понадобится, благодаря макросы ввода-вывода):

implicit def pcIso =
  Iso.hlist(PropositionContent.apply _, PropositionContent.unapply _)

Мы также определим псевдоним для этого конкретного типа обновления (что не является абсолютно необходимым, но сделайте следующие примеры более краткими):

type PCUpdate = Option[String] :: Option[String] :: HNil

И наконец:

scala> val pc = PropositionContent("some title", "some content")
pc: PropositionContent = PropositionContent(some title,some content)

scala> val u1: PCUpdate = Some("another title") :: None :: HNil
u1: PCUpdate = Some(another title) :: None :: HNil

scala> val u2: PCUpdate = Some("newest title") :: Some("new content") :: HNil
u2: PCUpdate = Some(newest title) :: Some(new content) :: HNil

scala> append(u1, u2)
res0: PCUpdate = Some(newest title) :: Some(new content) :: HNil

scala> update(pc, append(u1, u2))
res1: PropositionContent = PropositionContent(newest title,new content)

Это то, что мы хотели.

Ответ 2

Просто портирован код Travis (который я не понимаю) на Shapeless 2.1:

import shapeless._
import shapeless.ops.hlist._

object orElser extends Poly1 {
  implicit def default[A]: Case[Option[A]] { type Result = Option[A] => Option[A] } = at[Option[A]] {
    oa => (o: Option[A]) => oa orElse o
  }
}

object getOrElser extends Poly1 {
  implicit def default[A]: Case[Option[A]] { type Result = A => A } = at[Option[A]] {
    oa => (a: A) => oa getOrElse a
  }
}

import UnaryTCConstraint._

def append[U <: HList: *->*[Option]#λ, F <: HList](u: U, v: U)
                                                  (implicit mapper: Mapper.Aux[orElser.type, U, F],
                                                            zipper: ZipApply.Aux[F, U, U]): U =
  v map orElser zipApply u

def update[T, L <: HList, F <: HList, U <: HList](t: T, u: U)
                                                 (implicit gen: Generic.Aux[T, L],
                                                           mapped: Mapped.Aux[L, Option, U],
                                                           mapper: Mapper.Aux[getOrElser.type, U, F],
                                                           zipper: ZipApply.Aux[F, L, L]) =
  gen from (u map getOrElser zipApply (gen to t))

API не поврежден.