0

I would like to use some case class features like the following:

val base: Base = new Derived
println(base.copy())

I cannot define both Base and Derived as case classes. And making any of them case class raises compiler error: Cannot resolve symbol copy. So, there is a workaround with dispatching case class metods to children:

abstract class BaseWorkaround(name: String) {
  def dispatchCopy: BaseWorkaround
}
case class DerivedWorkaround(name: String) extends BaseWorkaround(name) {
  override def dispatchCopy: BaseWorkaround = copy()
}

val abbie: BaseWorkaround = DerivedWorkaround("Abbie")
println(abbie.dispatchCopy)

The workaround works, but looks cumbersome. Is there more elegant way to perform this behaviour?

Loom
  • 9,768
  • 22
  • 60
  • 112

2 Answers2

3

Fundamentally, you cannot inherit a copy because base class' copy method wouldn't know how to copy subclass' fields.

What you are doing works, but, if you think about it, it is rather useless: case classes are (or, should be by design) immutable, so, if foo is an instance of a case class, then foo and foo.copy() are pretty much the same object for all practical purposes, creating these copies instead of just using foo everywhere is just pure waste of memory. (You can also just use clone instead of copy, if that's what you are really looking for for some reason - just make Base extend Cloneable)

The reason copy exists, if because it actually takes parameters (the actual definition of Derived.copy is def copy(name: String = name)) to facilitate creating instances of the class that are different from the original, i.e.: val foo = Derived("foo"); val bar = foo.copy("bar") ...

So, if you know what parameters your case classes will have, you can do something like this:

abstract class Base[T <: Base] { 
  def name: String; 
  def copy(name: String = name): T
}

case class Derived1(name: String) extends Base[Derived1]
case class Derived2(name: String) extends Base[Derived2]

val p: Base[_] = Derived1("foo")
val pCopy = p.copy()
val s: Base[_] = Derived2("bar")
val sCopy = s.copy

Note, that Derived1 and Derived2 are almost the same thing though (both are really just a "glorified String"). While this may still be useful in some cases (e.g., define different kinds of application-specific errors with a detailed message), the major limitation of this approach is that you cannot define subclasses with different set of parameters than you have defined in Base:

// This won't compile:
class Derived3(name: String, code: Int) extends Base[Derived3] 
Dima
  • 39,570
  • 6
  • 44
  • 70
1

You can refresh the information why inheritance of case classes is bad and what possible workarounds are:

Scala case class inheritance

Extend case class from another case class

Should I use the final modifier when declaring case classes?

case class inheriting another class/trait

What is *so* wrong with case class inheritance?

Inheriting properties among case class

Case classes are intended for algebraic data types in functional programming:

sealed trait Base
case class Derived1(i: Int) extends Base
case class Derived2(s: String) extends Base

OOP and inheritance with them are weird. You should prefer composition over inheritance:

case class Base(s: String)
case class Derived(i: Int, b: Base)

There are alternatives to case classes:

  • Contraband code generation

http://eed3si9n.com/contraband-an-alternative-to-case-class/

https://www.scala-sbt.org/contraband/

https://www.scala-sbt.org/1.x/docs/Datatype.html (sbt-datatype)

https://github.com/sbt/contraband

https://www.reddit.com/r/scala/comments/5xu085/contraband_an_alternative_to_case_class/

  • Stalagmite @data

https://gitlab.com/fommil/attic/tree/master/stalagmite

  • Data-class @data

https://github.com/alexarchambault/data-class

  • Scalameta @data, @root/@branch/@leaf, @root/@branch/@ast

https://github.com/scalameta/scalameta/blob/v4.7.5/scalameta/common/shared/src/main/scala/org/scalameta/data/data.scala#L9-L21

https://github.com/scalameta/scalameta/blob/v4.7.5/scalameta/common/shared/src/main/scala/org/scalameta/adt/Adt.scala#L11-L19

https://github.com/scalameta/scalameta/blob/v4.7.5/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/root.scala#L11

https://github.com/scalameta/scalameta/blob/v4.7.5/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/branch.scala#L11

https://github.com/scalameta/scalameta/blob/v4.7.5/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala#L13

Generate apply methods creating a class

how to efficiently/cleanly override a copy method

Prohibit generating of apply for case class

  • generic derivation with type classes

https://svejcar.dev/posts/2019/09/26/better-tostring-alternative-for-case-classes/

https://github.com/milessabin/shapeless/blob/main/examples/src/main/scala/shapeless/examples/alacarte.scala

https://github.com/milessabin/shapeless/blob/main/core/shared/src/test/scala/shapeless/alacarte.scala


Let's think what signature copy in Base should have. My understanding is that you meant

sealed trait Base {
  type This <: Base
  def name: String
  def age: Int
  def copy(name: String = name, age: Int = age): This
}
case class Derived(name: String, age: Int) extends Base {
  override type This = Derived
  override def copy(name: String = name, age: Int = age): Derived = Derived(name, age)
}

or

sealed trait Base {
  def name: String
  def age: Int
  def copy(name: String = name, age: Int = age): Base
}
case class Derived(name: String, age: Int) extends Base {
  override def copy(name: String = name, age: Int = age): Derived = Derived(name, age)
}

In principle, def copy in Derived, def copy in Base, def name, def age in Base can be generated from case class Derived(name: String, age: Int). But what if there are several inheritors of Base? Doesn't Base know too much now about Derived?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66