2

The problem is that I have some independent case classes with different fields. I wanted to write a method that returns instance of the case class of given type with one of parameters changed.

def updateParam[C : ClassTag](c: C, paramName: String, paramValue: Any): C = ...

I took advantage of this answer, but it involves the use of reflection mechanism which is not so beautiful. Is there a possibility to avoid using reflection or maybe at least hide it under the hood of some library?

pavelbel
  • 21
  • 3
  • Is there any good reason to not just use the `copy` method? `c.copy(paramName = paramValue)` does exactly that, but in type safe way, without any `Any`s. – Andrey Tyukin Apr 10 '18 at 15:29

1 Answers1

2

If you want to invoke same function on instances of different classes, then it is polymorphism.

If those classes are unrelated, the polymorphism is ad-hoc.

The usual way to implement ad-hoc polymorphism in Scala is by using typeclasses. Here, you can implement the typeclass Copyable:

trait Copyable[C] {
  def copy(c: C, paramName: String, value: Any): C
}

and implement your method as follows:

def updateParam[C: Copyable](c: C, paramName: String, paramValue: Any): C = {
  implicitly[Copyable[C]].copy(c, paramName, paramValue)
}

Now, if you have two unrelated classes, which somehow ended up unrelated despite the fact that you want to use them in the same context:

case class Foo(paramName: String, foo: Int)
case class Bar(paramName: String, bar: Option[Double])

you can provide type-classes that make them Copyable:

implicit object FooIsCopyable extends Copyable[Foo] {
  def copy(f: Foo, paramName: String, value: Any): Foo = {
    if (paramName == "paramName") {
      f.copy(paramName = value.asInstanceOf[String])
    } else if (paramName == "foo") {
      f.copy(foo = value.asInstanceOf[Int])
    } else {
      throw new IllegalArgumentException("Foo has no param `" + paramName + "`")
    }
  }
}

implicit object BarIsCopyable extends Copyable[Bar] {
  def copy(b: Bar, paramName: String, value: Any): Bar = {
    if (paramName == "paramName") {
      b.copy(paramName = value.asInstanceOf[String])
    } else if (paramName == "bar") {
      b.copy(bar = value.asInstanceOf[Option[Double]])
    } else {
      throw new IllegalArgumentException("Bar has no param `" + paramName + "`")
    }
  }
}

and then use your method like this:

val f = Foo("hello", 42)
val b = Bar("bye", Some(58.0))

println(updateParam(f, "paramName", "hello, world"))
println(updateParam(b, "paramName", "obey"))

which then prints:

Foo(hello, world,42)
Bar(obey,Some(58.0))

However, while you can do this, it's not entirely clear why you would want to. How did it happen that Foo and Bar end up in the same situation, so that you end up with essentially duck-typing, requiring method copy? Maybe you should eliminate the root-cause in the first place.


You might also want to take a look at lenses.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Thanks for your answer! But it means that for any new case class I'll have to implement implicit object with copy logic. Is there any other option? – pavelbel Apr 11 '18 at 06:56
  • As for the entire situation. I have a method where lambda with some logic passed depending on type (which is presented by case class). I noticed that all the difference is just in passing parameter to change in object. `(x, c) => c.copy(paramName = x)` I wanted to generalize it somehow or even better to get rid of these lambdas passing at all. – pavelbel Apr 11 '18 at 07:08
  • @pavelbel As I briefly mentioned in the end, the `(x, c) => c.copy(paramName = x)` thing looks very much like `(lens[Foo] >> 'paramName).set(c)(x)` (see [here](https://www.scala-exercises.org/shapeless/lenses)). Maybe the thing into which you are passing those lambdas can also be abstracted away by some other "optics". But I'm not sure, I don't know enough about your use case. Maybe you could consider updating your question with more details about the underlying problem, and eventually tag it as `shapeless`, if you deem it useful. Then maybe someone more knowledgeable about lenses could answer – Andrey Tyukin Apr 11 '18 at 12:18