Search code examples
scalatypeclassscala-3

Scala 3: Deriving a typeclass for conversion between a case class and a tuple type


I am aware that in Scala 3 it is now possible to convert between case class instances and tuples using Tuple.fromProductTyped and summon[Mirror.Of].fromProduct.

I would like to be able to define a typeclass that witnesses that a case class T can be converted to and from (A, B) using the aforementioned methods.

I've defined the typeclass like so:

trait IsPair[T, A, B]:
  def toTuple(t: T): (A, B)
  def fromTuple(ab: (A, B)): T

How can we derive IsPair for any conforming case class?

With the derivation in place, the following should compile and run without error:

case class Foo(a: Int, b: String)
val fooIsPair = summon[IsPair[Foo, Int, String]]

val foo = Foo(42, "qwerty")
val tuple = (42, "qwerty")

assert(fooIsPair.toTuple(foo) == tuple)
assert(fooIsPair.fromTuple(tuple) == foo)

Solution

  • You can just use a Mirror but restrict it to be for pair-like case classes:

    import scala.deriving.Mirror
    
    sealed trait IsPair[T]:
      type A
      type B
    
      def toTuple(t: T): (A, B)
      def fromTuple(ab: (A, B)): T
    
    object IsPair:
      type Aux[T, X, Y] = IsPair[T] { type A = X; type B = Y }
    
      inline def apply[T](using ev: IsPair[T]): ev.type = ev
    
      given instance[T <: Product, X, Y](using
        ev: Mirror.Product { type MirroredType = T; type MirroredMonoType = T; type MirroredElemTypes = (X, Y) }
      ): IsPair[T] with
        override final type A = X
        override final type B = Y
    
        override def toTuple(t: T): (X, Y) =
          Tuple.fromProductTyped(t)
        override def fromTuple(xy: (X, Y)): T =
          ev.fromProduct(xy)
    end IsPair
    

    That way we can do this:

    val foo = Foo(42, "qwerty")
    val tuple = (42, "qwerty")
    
    val fooIsPair = IsPair[Foo]
    
    assert(fooIsPair.toTuple(foo) == tuple)
    assert(fooIsPair.fromTuple(tuple) == foo)
    

    You can see the code running here.