Transform a case class to another by unwrapping types in Scala 3

549 Views Asked by At

I have a enum that represents a container and two case classes:

enum Container[+A]:
  case Value(value: A)
  case Default(default: A)

  def get: A = this match
    case Value(value)     => value
    case Default(default) => default

case class PersonTemplate(name: Container[String], age: Container[Int], enabled: Container[Boolean])
case class Person(name: String, age: Int, enabled: Boolean)

and I want to write a generic function in Scala 3 that converts all case classes like PersonTemplate into their counterpart like Person, something like:

def extract[I <: Product, O <: Product](input: I): O = ???

val initial = PersonTemplate(Value("John"), Default(12), Default(true))
val final = extract[PersonTemplate, Person](initial)
// Result: Person("John", 12, true)

I tried several approaches but none of them was succesfull and mainly because I don't understand how to use Scala 3 Tuple that to me looks different from Scala 2 Shapeless' HList (and even in shapeless I'm not that good).

My overall approach was:

  1. Convert the case class in a Tuple. For this I found Tuple.fromProductTyped
  2. Constrain each element to be a Container[_]. I found Tuple.IsMappedBy to guarantee the tuple has the correct shape and Tuple.InverseMap that seems to extract the type inside the container. I'm not sure where to put this code, though.
  3. Apply a (poly?)function to each value that calls Container.get. With the little I found around the net I ended up using a lot of .asInstanceOf and it didn't seem right to me.
  4. Convert the resulting Tuple to the output type using summon[Mirror.Of[O]].fromProduct(output)

For sake of completeness, this is the last code I tried, that of course doesn't work:

def resolve[I <: Product: Mirror.ProductOf, O: Mirror.ProductOf](input: I): O =
  val processed =
    Tuple
      .fromProductTyped(input)
      .map { [T] => (value: T) => ??? }

  summon[Mirror.Of[O]].fromProduct(processed)

type ExtractG = [G] =>> G match {
  case Container[a] => a
}

def process[I <: Tuple, O <: Tuple](input: I)(using Tuple.IsMappedBy[Container][I]): O =
  input.map { [A] => (a: A) =>
    a.asInstanceOf[Container[_]].get.asInstanceOf[ExtractG[A]]
  }.asInstanceOf[O]
1

There are 1 best solutions below

2
On BEST ANSWER

Well if you don't mind a bit of casting, you can do this:

def unwrapper[From <: Product, To](
  using To: Mirror.ProductOf[To],
  From: Mirror.ProductOf[From],
  ev: From.MirroredElemTypes =:= Tuple.Map[To.MirroredElemTypes, Container]
): (From => To) =
  from => To.fromProduct {
    from.productIterator
        .toArray
        .map(_.asInstanceOf[Container[_]].get)
        .foldRight[Tuple](EmptyTuple)(_ *: _)
  }

@main def run =
  import Container._
  val unTemplate = unwrapper[PersonTemplate, Person]
  println(unTemplate(PersonTemplate(Value("foo"), Default(42), Default(false))))

Requested From and ev only serve to prove the type-safety of all the casting. IMO the mirror machinery lacks in ability to operate on things in a type-safe way without macros like shapeless can.