Return copy of case class from generic function wi

2020-06-18 03:01发布

问题:

I want to get rid of a runtime cast to a generic (asInstanceOf[A]) without implicit conversions.

This happens when I have a fairly clean data model consisting of case classes with a common trait and want to implement a generic algorithm on it. As an example the resulting algorithm should take a class of type A that is a subclass of the trait T and is supposed to return a copy of the concrete class A with some updated field.

This is easy to achieve when I can simply add an abstract copy-method to the base trait and implement that in all sub-classes. However this potentially pollutes the model with methods only required by certain algorithms and is sometimes not possible because the model could be out of my control.

Here is a simplified example to demonstrate the problem and a solution using runtime casts.

Please don't get hung up on the details.

Suppose there is a trait and some case classes I can't change:

trait Share {
  def absolute: Int  
}

case class CommonShare(
    issuedOn: String, 
    absolute: Int, 
    percentOfCompany: Float) 
  extends Share

case class PreferredShare(
    issuedOn: String, 
    absolute: Int, 
    percentOfCompany: Float)
  extends Share

And here is a simple method to recalculate the current percentOfCompany when the total number of shares have changed and update the field in the case class

def recalculateShare[A <: Share](share: A, currentTotalShares: Int): A = {

  def copyOfShareWith(newPercentage: Float) = {
    share match {
      case common: CommonShare => common.copy(percentOfCompany = newPercentage)
      case preferred: PreferredShare => preferred.copy(percentOfCompany = newPercentage)
    }
  }

  copyOfShareWith(share.absolute / currentTotalShares.toFloat).asInstanceOf[A]
}

Some example invocations on the REPL:

scala> recalculateShare(CommonShare("2014-01-01", 100, 0.5f), 400)
res0: CommonShare = CommonShare(2014-01-01,100,0.25)

scala> recalculateShare(PreferredShare("2014-01-01", 50, 0.5f), 400)
res1: PreferredShare = PreferredShare(2014-01-01,50,0.125)

So it works and as far as I understand the .asInstanceOf[A] call will never fail but is required to make the code compile. Is there a way to avoid the runtime cast in a type-safe manner without implicit conversions?

回答1:

You have a couple of choices I can think of, and it mostly comes down to a balance of how general of a solution you want and how much verbosity you can tolerate.

asInstanceOf

Your solution feels dirty, but I don't think it's all that bad, and the gnarliness is pretty well contained.

Typeclass

A great approach to providing behavior to data types while still maintaining separation of concerns in your code is the Enrich Your Library / typeclass pattern. I wish I had a perfect reference for this, but I don't. Look up those terms or "implicit class", and you should be able to find enough examples to get the drift.

You can create a trait Copyable[A] { def copy(?): A } typeclass (implicit class) and make instances of it for each of your types. The problem here is that it's kind of verbose, especially if you want that copy method to be fully generic. I left its parameter list as a question mark because you could just narrowly tailor it to what you actually need, or you could try to make it work for any case class, which would be quite difficult, as far as I know.

Optics

Lenses were made for solving this sort of awkwardness. You may want to check out Monocle, which is a nice generic approach to this issue. Although it still doesn't really solve the issue of verbosity, it might be the way to go if you have this issue recurring throughout your project, and especially if you find yourself trying to make changes deep within your object graph.



回答2:

Here is a typeclass approach suggested by @acjay

trait Copyable[A <: Share] {
  def copy(share: A, newPercentage: Float): A
}

object Copyable {
  implicit val commonShareCopyable: Copyable[CommonShare] =
    (share: CommonShare, newPercentage: Float) => share.copy(percentOfCompany = newPercentage)

  implicit val preferredShareCopyable: Copyable[PreferredShare] =
    (share: PreferredShare, newPercentage: Float) => share.copy(percentOfCompany = newPercentage)
}

implicit class WithRecalculateShare[A <: Share](share: A) {
  def recalculateShare(currentTotalShares: Int)(implicit ev: Copyable[A]): A =
    ev.copy(share, share.absolute / currentTotalShares.toFloat)
}

CommonShare("2014-01-01", 100, 0.5f).recalculateShare(400)      
// res0: CommonShare = CommonShare(2014-01-01,100,0.25)

PreferredShare("2014-01-01", 50, 0.5f).recalculateShare(400)
// res1: PreferredShare = PreferredShare(2014-01-01,50,0.125)