2

In Scala (both 2 and 3), the copy method of case classes is synthetic, and so cannot be generalized using simple type clauses. In Scala 2, this led to generically processing case classes being done via shapeless. In Scala 3, however, support for abstracting over product types is substantially improved, alongside with other new language features, which leads to the following question:


In Scala 3, how can one create a generic copy function for all cases classes, while minimizing implementation complexity, usage complexity, and runtime resource cost?

The following, in particular, should be avoided:

  • explicit, instantiated "witness" objects for type-recursive processing, like in this example;
  • any need to explicitly define generic types for the "genericy copy" function call: ideally, all generics and givens should be infer-able;
  • use of any external libraries, unless they fulfill the criteria defined here.

What has been tried:

Let's suppose we have the following two test case classes:

case class CC1(a: String)

case class CC2(a: String, b: Int)

A naive first attempt would be this:

def genCopyV1[CC <: Product](
    cc: CC,
    copy: ccMirror.MirroredElemTypes => ccMirror.MirroredElemTypes
)(using
    ccMirror: ProductOf[CC]
): CC =
  ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[ccMirror.MirroredElemTypes]))

This fails with two Not found: ccMirror errors, since Scala 3 only supports dependent types in return values.

We can maybe try to improve it with some path-dependent types:

def genCopyV2[CC <: Product](
    cc: CC,
    copy: ProductOf[CC]#MirroredElemTypes => ProductOf[CC]#MirroredElemTypes
)(using
    ccMirror: ProductOf[CC]
): CC =
  ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[ccMirror.MirroredElemTypes]))

This actually compiles, but only until trying out actual usage:

println(generic.genCopyV2(CC1("a"), _ => Tuple1("b")))
//Found:    Tuple1[String]
//Required: deriving.Mirror.ProductOf[CC1]#MirroredElemTypes

println(generic.genCopyV2(CC2("a", 1), (a, b) => (a + "b", b + 1)))
// Wrong number of parameters, expected: 1

the errors are likely caused by the ProductOf[CC]#MirroredElemTypes not actually being bound to the particular Mirror instance resolved for ProductOf[CC].

Finally, if we bind the tuple representation type* :

def genCopyV3[CC <: Product, TupleType <: Tuple](cc: CC, copy: TupleType => TupleType)(using
    ccMirror: ProductOf[CC],
    tupleEqEnv: TupleType =:= ccMirror.MirroredElemTypes
): CC =
  ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[TupleType]))

we can finally use the function:

println(generic.genCopyV3[CC1, String *: EmptyTuple](CC1("a"), _ => Tuple1("b")))
// CC1(b)

println(generic.genCopyV3[CC2, (String, Int)](CC2("a", 1), (a, b) => (a + "b", b + 1)))
// CC2(ab,2)

println(generic.genCopyV3(CC2("a", 1), (a: String, b: Int) => (a + "b", b + 1)))
// CC2(ab,2)

Of course, this is not very useful, since the calling code needs to either explicitly specify the generic parameters, or bind them some other way (like in the argument function signature of the final usage example).


* for simplicity of use (not needing Tuple1), we can also add the 1-element case class special case:

def genCopyV3[CC <: Product, Single](cc: CC, copy: Single => Single)(using
    ccMirror: ProductOf[CC],
    tupleEqEnv: Single *: EmptyTuple =:= ccMirror.MirroredElemTypes,
    singleNotTupleEnv: NotGiven[Single =:= Tuple]
): CC =
  ccMirror.fromProduct(Tuple(copy(cc.productElement(0).asInstanceOf[Single])))
mikołak
  • 9,605
  • 1
  • 48
  • 70
  • **Scala 3** just included **Shapeless** inside it, nothing changed in this regard. - anyways, abstracting `copy` is usually meaningless since typing it is impossible. – Luis Miguel Mejía Suárez Apr 09 '22 at 22:57
  • @LuisMiguelMejíaSuárez : care to elaborate on what you mean by "typing is impossible"? – mikołak Apr 10 '22 at 08:03
  • Sure for `case class Foo(id: Int, name: String)` then `copy` has type `(Foo, Int, String) => Foo`, whereas for `case clas Bar(x: Double, y: Double, z: Double)` then `copy` has type `(Bar, Double, Double, Double) => Bar` - So now, tell, what would be the signature of a generic copy method? – Luis Miguel Mejía Suárez Apr 10 '22 at 13:49
  • I see. Yes, in this case explicit typing is difficult to represent explicitly due to the arity issues, but this is exactly why (co)product type constructs exist. Note that I'm not in any way requesting a perfect, end-to-end type consistency, just that the input and ultimate output type contracts are mutually consistent - anywhere in between, I don't care how the type sausage is made, so to speak (as evidenced by the `asInstanceOf`, which is used in the exact same way in Scala 3's `Tuple.fromProductTyped` anyway). – mikołak Apr 10 '22 at 22:01
  • The point is how to even call it? – Luis Miguel Mejía Suárez Apr 10 '22 at 22:06
  • Hi mikołak :) I'm not sure if I understand correctly, you want to do something similar to what mappers like `chimney` or `henkan` did in Scala 2, right? – Krzysztof Atłasik Apr 12 '22 at 13:38

0 Answers0