0

I'm trying to write a boilerplate function that can take a pair of generic case class objects and perform some operations on their set of vals and then return a new instance of the case class.

On a more detailed level that is not the intended focus of this question, the operation I am performing is zipping the two two lists of vals and selectively deciding which item of the tuple is placed into the new instance by way of a List[Boolean] of flags that indicate the left or right object.

I have the majority of the concept written up, and to the best of my research so far my strategy is to convert the case class objects into a list of the vals (using Product.productIterator) and then eventually turn a list of vals into a tuple (SO reference) to feed into Function.tupled (SO reference) (which is available on case classes).

trait PermissionMask[B <: Product] {
    val permissionMask: List[Boolean]

    def mergePermissibleEdits(userObject: B, trustedObject: B) = {
      val possibleValues = (userObject.productIterator.toList) zip (trustedObject.productIterator.toList)
      val valuesWithFlags = possibleValues zip permissionMask

      val mergedObjectList = valuesWithFlags map { case ((userValue, trustedValue), userEditable) => if (userEditable) userValue else trustedValue }

      //TODO: Not possible, I don't have a direct reference to the case class since it's generic, not sure how to get the reference
      B.tupled(mergedObjectList) //I have implicit conversions to tuple1-22 from list
    }
  }

If I wasn't using generics and knew the concrete case class, I could simply call SomeCaseClass.tupled(mergedObjectList). However, since it's generic I am fuzzy on how (if even possible) I can make that same call.

Community
  • 1
  • 1
Rich
  • 2,805
  • 8
  • 42
  • 53
  • 2
    This sort of thing is not easy in general, and a lot of the work has been done for you in Shapeless. – Rex Kerr Oct 01 '14 at 19:38
  • Note that if your case class only has one element then the `apply` method is `Function1` which does not have a `tupled` function, 2+ have tupled. – Noah Oct 01 '14 at 20:36

1 Answers1

2

As I stated above you can't use tupled as it doesn't apply to all case class apply methods. Here's a version that's fairly hacky, but feasibly 'could' work. This takes the first constructor and applies your merged parameter list to it. Warning, this is very suspicious and highly breakable, I wouldn't use this in production:

  def mergePermissibleEdits[B <: Product : ClassTag](userObject: B, trustedObject: B, permissionMask: List[Boolean]) = {
    val possibleValues = userObject.productIterator.toList.map(_.asInstanceOf[AnyRef]) zip trustedObject.productIterator.toList.map(_.asInstanceOf[AnyRef])
    val valuesWithFlags = possibleValues zip permissionMask

    val mergedObjectList = valuesWithFlags map { case ((userValue, trustedValue), userEditable) => if (userEditable) userValue else trustedValue}

    implicitly[ClassTag[B]].runtimeClass.getConstructors.head.newInstance(mergedObjectList: _*).asInstanceOf[B]
  }

  case class Id(id1: Int, id2: String, id3: Double)

  println(mergePermissibleEdits(Id(1, "test", 3.0), Id(4, "works", 6.0), List(true, false, true)))
  //prints Id(1,works,3.0)
Noah
  • 13,821
  • 4
  • 36
  • 45
  • Thanks. Could you possibly expand on your concerns for using this code seriously? Is it mostly based on the breakability (such as if the runtime types of B, userObject, or trustedObject don't match seems hard to prevent) or are there further concerns? – Rich Oct 01 '14 at 21:46
  • @Rich There's no guarantee about the `permissionMask` size being the same size as the `possibleValues`, also this uses the first constructor it finds for `B`, this may not be the appropriate constructor, it also casts `Any` to `AnyRef` which is most likely ok, but if you have an `AnyVal` instead it will break. I understand what you're trying to do here, and I think shapeless might be a better usage with lenses (not that I have a good example for you) or just directly merging your case classes. – Noah Oct 02 '14 at 00:00
  • Thanks for the comments. The AnyRef vs AnyVal is the most alarming issue that I had not considered. At a higher level I will probably be better off to stop trying to force my way back to a case class in light of the limitations touched on throughout this and the linked questions. My end goal functional requirements don't depend on it, naivety just made it seem like a nice idea. I'll look into Shapeless, even if I don't end up using it the supporting documentation seems like it will be great to further my understanding of types and generics in Scala. – Rich Oct 02 '14 at 15:57
  • And for reference I found these articles illuminating on underlying issues with the type of generic polymorphism involved in my original stated goal. Part two in particular seem to conclude with some relevant observations [part 1](http://www.chuusai.com/2012/04/27/shapeless-polymorphic-function-values-1/) and [part 2](http://www.chuusai.com/2012/05/10/shapeless-polymorphic-function-values-2/) – Rich Oct 02 '14 at 19:19
  • Revisiting the cast of `Any` to `AnyRef` not working with 'AnyVal' (I assume you are referring to the `map(_.asInstanceOf[AnyRef])`), will it really break? Your example has two AnyVal in the Id case class (Int and Double) and I can confirm that it does in fact work like you demonstrated. Did I misinterpret your comment? – Rich Oct 02 '14 at 19:52
  • This works because scala is boxing this behind the scenes and turning `Int` into `Integer`. I doubt that you would run into issues with this, but any sort of casting is always a code smell that can lead to danger down the road. – Noah Oct 02 '14 at 20:41