Search code examples
scalagenericscase-class

scala function to operate on any case class


I've been trying ot create a function that does some inflection on any case class but can't figure out how to pull it off on non concrete generic types, what am I doing wrong?

Please note I can't change the original case classes passed to this function ** edit ** forgot to mention I'm using pre 2.13 scala and prefer a solution that doesn't rely on external libraries.

        def caseClassToArray[A](something: A) = {
          classOf[A]
          .getDeclaredFields
          .map { f: Field =>
          f.setAccessible(true)
          val res = (f.getName, f.get(something))
          f.setAccessible(false)
          res
        }
    }

Solution

  • Given what was discussed in comments, it seems the best solution would be a Diff typeclass and using Shapeless for the derivation for case classes.

    The first step is to define the typeclass.

    trait Diff[A] {
      def diff(a1: A, a2: A): A
    }
    

    You may need to adjust the definition for your exact use case.

    The next step is to provide support for basic std types like Int, String, List[A] for any A that has a Diff, etc

    object Diff {
      implicit final val IntDiff: Diff[Int] =
        new Diff[Int] {
          override def diff(i1: Int, i2: Int): Int =
            i1 - i2
        }
    
      ...
    }
    

    You may also want to add extension methods, implicitNotFound error message, and everything just like any other typeclass.
    That stuff is out of the scope of this answer.

    The next step is to use Shapeless to inductively derive an instance for any HList for whose all element types have a Diff associated.

    import shapeless.{:: => :!:, HList, HNil}
    
    sealed trait ReprDiff[R <: HList] extends Diff[R]
    object ReprDiff {
      implicit final val HNilReprDiff: ReprDiff[HNil] =
        new ReprDiff[HNil] {
          override final def diff(nil1: HNil, nil2: HNil): HNil = HNil
        }
      
      implicit def HConsReprDiff[H, T <: HList](
        implicit ev: Diff[H], tailReprDiff: ReprDiff[T]
      ): ReprDiff[H :!: T] = new ReprDiff[H :!: T] {
        override final def diff(hlist1: H :!: T, hlist2: H :!: T): H :!: T = {
          val headDiff = ev.diff(hlist1.head, hlist2.head)
          val tailDiff = tailReprDiff.diff(hlist1.tail, hlist2.tail)
          
          headDiff :: tailDiff
        }
      }
    }
    

    Finally, using Shapeless Generic magic we can provide an instance for any case class for which exist an instance of its HList representation.

    import shapeless.Generic
    
    sealed trait DerivedDiff[P <: Product] extends Diff[P]
    object DerivedDiff {
      implicit def instance[P <: Product, R <: HList](
        implicit gen: Generic.Aux[P, R], ev: ReprDiff[R]
      ): DerivedDiff[P] = new DerivedDiff[P] {
        override final def diff(p1: P, p2: P): P =
          gen.from(ev.diff(gen.to(p1), gen.to(p2)))
      }
    }
    

    gen.to turns any instance of a case class P into an HList that represents it.
    gen.from turns a matching instance of an HList into a P

    Now, all we need is a way to let our users derive the instance for their case classes as long as all fields have a Diff associated with them.
    I personally prefer the semiauto approach:

    // Somewhere, like in the companion object of Diff
    def derive[P <: Product](implicit ev: Deriving.DerivedDiff[P]): ev.type = ev
    

    With all that, you can just use it like this:

    final case class Foo(a: Int, b: Int)
    object Foo {
      implicit final val FooDiff: Diff[Foo] =
        Diff.derive[Foo]
    }
    

    Congratulations, you now can diff two Foo instances:

    val foo1 = Foo(1, 3)
    val foo2 = Foo(0, 5)
    
    val diff = Diff.diff(foo1, foo2)
    // res: Foo = Foo(1, -2)
    

    In this example we added an auxiliary method diff in the Diff companion object; you may provide any other API to call the diff operation.


    You can see the code running here.