In Scala 3 program I have several case classes that share a lengthy function that only differs slightly for each class, so it makes sense to make it generic. But here's the problem: I cannot come up with the solutions that will directly return a collection of a correct type without resorting to an ugly hack like asInstanceOf.
I simplified the code for a readability purpose:
enum TestStructure:
case Struct1(v1: Int)
case Struct2(v2: Int)
@tailrec def testNotWorking[T <: TestStructure](l: List[T], acc: List[T] = Nil): List[T] = l match
case Nil => acc.asInstanceOf[List[T]].reverse
case (s: Struct1) :: t =>
testNotWorking(t, s.copy(v1 = s.v1 1) :: acc)
case (s: Struct2) :: t =>
testNotWorking(t, s.copy(v2 = s.v2 1) :: acc)
@tailrec def testWorking[T <: TestStructure](l: List[T], acc: List[TestStructure] = Nil): List[T] = l match
case Nil => acc.asInstanceOf[List[T]].reverse
case (s: Struct1)::t =>
test(t, s.copy(v1 = s.v1 1)::acc)
case (s: Struct2) :: t =>
test(t, s.copy(v2 = s.v2 1) :: acc)
testWorking[Struct1](List(Struct1(1), Struct1(2))
Declaring acc as List[T] doesn't work here. Is there any way to solve this problem without runtime casting?
CodePudding user response:
Many thanks to Dmytro Mitin, the answers in the link he provided are really comprehensive. Too bad I can't upvote his comment. The slight problem is all the answers are geared towards Scala 2, and since Scala 3 doesn't support TypeTags, this option is out of the window. Nevertheless, a typeclass seems like the most elegant solution here:
enum TestStructure:
case Struct1(v1: Int)
case Struct2(v2: Int)
trait TestStructureId[A <: TestStructure]:
extension(t: A) def id: A
given TestStructureId[TestStructure.Struct1] with
extension (t: TestStructure.Struct1) def id: TestStructure.Struct1 = t
given TestStructureId[TestStructure.Struct2] with
extension (t: TestStructure.Struct2) def id: TestStructure.Struct2 = t
@tailrec def testTypeClasses[T <: TestStructure](l: List[T], acc: List[T] = Nil)(using TestStructureId[T]): List[T] = l match
case Nil => acc.reverse
case (s: TestStructure.Struct1) :: t =>
testTypeClasses(t, s.copy(v1 = s.v1 1).asInstanceOf[T] :: acc)
case (s: TestStructure.Struct2) :: t =>
testTypeClasses(t, s.copy(v2 = s.v2 1).asInstanceOf[T] :: acc)
testTypeClasses[TestStructure.Struct1](List(TestStructure.Struct1(1), TestStructure.Struct1(2)))
This way everything works perfect. If I pass a List[Struct1] or List[Struct2], the function returns the same type. But if I mix Struct1 and Struct2 together, the code won't compile. The only minor gripe I have is that it's not possible to declare an empty typeclass and I had to slap on the useless id function.