Search code examples
scalacase-classshapeless

How to represent a partial update on case classe in Scala ?


I have the following case class :

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

And I would like to represent a partial modification of it as Data.

One way would be to create the case class :

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

and then some methods :

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 = {...}

}

But it's a bit boilerplaty !

I think a case class PropositionContent[M[_]](title:M[String],content:M[String]) will not really solve the stuff, and I don't know how to use Shapeless to solve the stuff.

So do you have an idea ?


Solution

  • Here's a relatively boilerplate-free approach using Shapeless. First we define some polymorphic versions of the relevant functions on 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
      }
    }
    

    We'll represent an update as an HList where every element is an Option, and we can write an append method that allows us to append two updates:

    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)
    

    And finally we can write our update method itself:

    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)
    

    Now we need just a little bit of boilerplate (in the future this won't be necessary, thanks to inference-driving macros):

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

    We'll also define an alias for this specific update type (which isn't strictly necessary, but will make the following examples a little more concise):

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

    And finally:

    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)
    

    Which is what we wanted.