7

I have a lot of similar case classes which mean different things but have the same argument list.

object User {
  case class Create(userName:String, firstName: String, lastName: String)
  case class Created(userName:String, firstName: String, lastName: String)
}

object Group {
  case class Create(groupName:String, members: Int)
  case class Created(groupName:String, members: Int)
}

Given this kind of a setup, I was tired of writing methods that take an argument of type Create and return an argument of type Created. I have tons of test cases that do exactly this kind of thing.

I could write a function to convert one case class into the other. This function converts User.Create into User.Created

def userCreated(create: User.Create) = User.Create.unapply(create).map((User.Created.apply _).tupled).getOrElse(sys.error(s"User creation failed: $create"))

I had to write another such function for Group. What I'd really like to have is a generic function that takes the two types of the case classes and an object of one case class and converts into the other. Something like,

def transform[A,B](a: A):B

Also, this function shouldn't defeat the purpose of reducing boilerplate. Please feel free to suggest a different signature for the function if that's easier to use.

  • Do you expect something like that `convert(a).to[B]`? – Sergii Lagutin Aug 11 '15 at 18:42
  • Is it important that all of these paired classes have the create/created name or is your requirement purely that they can be paired and that the one function will transform the first class of any such pair into the second? – itsbruce Aug 11 '15 at 23:16

2 Answers2

9

Shapeless to the rescue!

You can use Shapeless's Generic to create generic representations of case classes, that can then be used to accomplish what you're trying to do. Using LabelledGeneric we can enforce both types and parameter names.

import shapeless._

case class Create(userName: String, firstName: String, lastName: String)
case class Created(userName: String, firstName: String, lastName: String)
case class SortOfCreated(screenName: String, firstName: String, lastName: String)

val c = Create("username", "firstname", "lastname")

val createGen = LabelledGeneric[Create]
val createdGen = LabelledGeneric[Created]
val sortOfCreatedGen = LabelledGeneric[SortOfCreated]

val created: Created = createdGen.from(createGen.to(c))

sortOfCreatedGen.from(createGen.to(c)) // fails to compile
Ryan
  • 7,227
  • 5
  • 29
  • 40
  • Nice solution! However, there is a possible pitfall: as `Generic` works by converting objects to `HList` it doesn't ensure that corresponding parameter names of source and target objects match. Thus if you have different params with same type or different params order you'll get wrong result of conversion in runtime. There [is plenty of java utils](http://stackoverflow.com/a/1432956/1349366) that perform mapping by reflection that will solve this issue. – Aivean Aug 11 '15 at 20:05
  • @Aivean That's a solid point. Depends on if you care about the parameter names or just the types—this is a case where value classes would be clutch. – Ryan Aug 11 '15 at 20:11
  • 2
    @Aivean This can be directly fixed by using `LabelledGeneric`, which would require exact correspondence of case classes parameter names and types and their order. – Kolmar Aug 11 '15 at 20:20
  • I wonder if it's possible to define a generic function like in OP: `def transform[A : Generic,B : Generic](a: A):B`. I can't find an implicit parameter to prove to the compiler the case class schema equality. `implicit ev: Generic[A]#Repr =:= Generic[B]#Repr` fails to compile. – Kolmar Aug 11 '15 at 20:21
  • @Kolmar I tried defining a function like `transform` but also didn't have any luck. – Ryan Aug 11 '15 at 20:21
  • One way is something like `def transform[A, B](a: A)(implicit ga: Generic[A], gb: Generic[B]): B = gb.from(ga.to(a).asInstanceOf[gb.Repr])`, but it completely ignores type safety. – Kolmar Aug 11 '15 at 20:27
  • @Kolmar you can define your own "witness of generic equality" type, create implicit instances of that type for each pair of your possible conversions and request it as implicit argument in `transform` function. – Aivean Aug 11 '15 at 20:36
5

For the record, here is the simplest typesafe syntax I've managed to implement:

implicit class Convert[A, RA](value: A)(implicit ga: Generic.Aux[A, RA]) {
  def convertTo[B, RB](gb: Generic.Aux[B, RB])(implicit ev: RA =:= RB) =
    gb.from(ga.to(value))
}

And it can be used like this:

case class Create(userName: String, firstName: String, lastName: String)
case class Created(userName: String, firstName: String, lastName: String)

val created = Create("foo", "bar", "baz").convertTo(Generic[Created])

Or the same thing with LabelledGeneric to achieve better type safety:

implicit class Convert[A, RA](value: A)(implicit ga: LabelledGeneric.Aux[A, RA]) {
  def convertTo[B, RB](gb: LabelledGeneric.Aux[B, RB])(implicit ev: RA =:= RB) =
    gb.from(ga.to(value))
}

val created = Create("foo", "bar", "baz").convertTo(LabelledGeneric[Created]))
Kolmar
  • 14,086
  • 1
  • 22
  • 25