3

I have the following case class:

case class MyClass[A,B](a:A, b:B)

I want to add a function like unzip to MyClass so if A and B are Tuple types then I want to extract them like the following:

val item = MyClass[(Int,String), (Int,String)]((2,"two"), (3,"three"))
val item_left = MyClass(item.a._1, item.b._1)
val item_right = MyClass(item.a._2, item.b._2)

How should I do that and check the type for tuple in compile time? I don't want to define it in the companion object and I want it to be a function in MyClass. I know that I can define an implicit function but is it the only way?

Omid
  • 1,959
  • 25
  • 42

2 Answers2

5

You can use the <:< type class to prove that A and B are sub-types of Tuple2, so that you can decompose them. That is, we can write an unzip method to have the some free type parameters that would be the decomposed ordinate types (call them A1, A2, B1, and B2). Then, we require evidence that A <:< (A1, A2) and B <:< (B1, B2). If the sub-typing relationship is true, the compiler will find instances of these type classes, which we can use to finalize the conversion. That is, A <:< (A1, A2) extends the function A => (A1, A2).

case class MyClass[A, B](a: A, b: B) {
  def unzip[A1, A2, B1, B2](implicit
      ev1: A <:< (A1, A2),
      ev2: B <:< (B1, B2)
    ): (MyClass[A1, A2], MyClass[B1, B2]) = {
    val (a1, a2) = ev1(a)
    val (b1, b2) = ev2(b)
    (MyClass(a1, a2), MyClass(b1, b2))
  }
}

In action:

scala> MyClass((2, "two"), (3, "three")).unzip
res6: (MyClass[Int,String], MyClass[Int,String]) = (MyClass(2,two),MyClass(3,three))

For non-tuples:

scala> MyClass(1, 2).unzip
<console>:14: error: Cannot prove that Int <:< (A1, A2).
       MyClass(1, 2).unzip
                     ^
Michael Zajac
  • 55,144
  • 7
  • 113
  • 138
  • My semantics of what values go where might not be exactly what you want, but you can just re-arrange them to fit your requirements. – Michael Zajac Dec 30 '16 at 21:11
  • where can I find doc related to <:< and what do you call it? – Omid Dec 30 '16 at 22:21
  • even there is no need to explicitly use ev1(a), a._1 and a._2 work because of that implicit functions – Omid Dec 30 '16 at 22:31
  • Note that in the collections library, instead of `Traversable[A]#unzip[A1, A2]` taking an `A <:< (A1, A2)` (requiring that `A` actually *be* a tuple), it takes `A => (A1, A2)`, which relaxes the restriction so that only an implicit conversion to a tuple must be present. – HTNW Dec 31 '16 at 02:11
0

Michael's answer is excellent. You could also go a much simpler route if you're willing to require that A and B are sub-types of Product in the declaration of your case class:

case class MyClass[A <: Product, B <: Product](a:A, b:B) {
  def item_left = (a.productIterator.toList(0), b.productIterator.toList(0))
  // etc.
}

Now, you can write:

val x = MyClass((2,"two"), (3,"three"))
x.item_left

which results in:

(2,3)

of type (Any,Any).

I suggest this alternative only because it was unclear to me just how complex you're willing to be. I hope not to elicit any down votes ;)

Phasmid
  • 923
  • 7
  • 19