Search code examples
scalashapelessstructural-typing

Create "enriched" type from case class with Shapeless in Scala


I have this example code:

import java.util.UUID

import shapeless.LabelledGeneric
import shapeless.record._
import shapeless.syntax.singleton._

object LabelTest extends App {

  case class IncomingThing(name: String, age: Int)
  case class DatabaseIncomingThing(name: String, age: Int, id: UUID)

  val genIncoming = LabelledGeneric[IncomingThing]
  val genDatabase = LabelledGeneric[DatabaseIncomingThing]

  val thing = IncomingThing("John", 42)

  val structuralVersionOfIncomingThing = genIncoming.to(thing)

  val updated = genDatabase.from(structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID()))

  println(updated) // DatabaseIncomingThing(John,42,a45081f2-4ed5-4d2b-8fd9-4d8986875ed7)

}

Which is nice because I don't have to write the boilerplate that copies over all the fields from IncomingThing to DatabaseIncomingThing. However, I would love not having to maintain both those types since there is a very clear relationship between the two (one has id, the other doesn't).

Is there a way to create a type from a given case class by either adding or removing one field? I imagine something like

type IncomingThing = withoutField[DatabaseIncomingThing]('id)

Or something to that effect.


Solution

  • Instead of DatabaseIncomingThing

    val updated: DatabaseIncomingThing = 
      genDatabase.from(structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID()))
    

    you can work with raw HList

    val updated1: FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil = 
      structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID())
    val updated2: FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil = 
      updated1 - 'id
    

    On type level

    implicitly[Remover.Aux[FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil,
      Witness.`'id`.T,
      (UUID, FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil)]]
    
    implicitly[Updater.Aux[
      FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil,
      FieldType[Witness.`'id`.T, UUID],
      FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil]]
    

    You can create your type class

    trait WithoutField[A, K] {
      type Out <: HList
    }
    
    object WithoutField {
      type Aux[A, K, Out0 <: HList] = WithoutField[A, K] { type Out = Out0 }
      def instance[A, K, Out0 <: HList]: Aux[A, K, Out0] = new WithoutField[A, K] { type Out = Out0 }
    
      implicit def mkWithoutField[A, L <: HList, K, T, L1 <: HList](implicit
        labelledGeneric: LabelledGeneric.Aux[A, L],
        remover: Remover.Aux[L, K, (T, L1)]): Aux[A, K, L1] = instance
    }
    

    and use it

    def foo[Out <: HList](implicit withoutField: WithoutField.Aux[DatabaseIncomingThing, Witness.`'id`.T, Out]) = {
      // now you can use type Out inside
      type IncomingThing = Out
      ???
    }