Modular case class features

Case classes offer a bunch of boilerplate-avoiding convenience:

  • structural equals and hashcode
  • structural toString (without parameter names, unfortunately)
  • unapply
  • Mirror instance
  • copy
  • (Have I missed any?)

However, they also impose a lot of restrictions. I often find myself wanting some of the features of case classes, without having to opt in for the whole package.

Are there more modular ways to get some of these features without the boilerplate?

The documentation mentions Show… would that be the way to avoid boilerplate for toString? And is it referring to this Show? More generally, is type class derivation the way to go?

2 Likes

Show is a common name for a typeclass providing a string representation like toString (as the docs mention it is also called that in Haskell). The Show from the cats library is a possible implementation of that typeclass, but the documentation doesn’t refer to that one specifically, the examples actually implement it themself (they call it Showable, but it’s the same thing).

As for if that would be a way to avoid boilerplate for toString, it depends. As the typeclass provides an implementation external to the datatype, it can’t override toString, so you’ll have to change any callsites from obj.toString to obj.show. Also, you’ll have to be aware of all the places toString is used implicitly, e.g. in string interpolation. Cats provides its own interpolator, so you can write show"text $var text" instead of s"text $var text" to use the show method instead of toString.
Usually Show is used, because you can then restrict methods to accept only types with an implementation, which isn’t possible with toString, as it is defined on Object, so everything has it.

For your specific requirement of not using case classes, it may be difficult. Generally, typeclass derivation is a nice way of modularly adding functionality to a type, but all the implementations I’ve used or written used parts provided by case classes for deriving (case classes being a subclass of Product, or Mirrors with Scala 3). For example, the derivation implementation of Show from cats won’t work with a non-case class:

% scala-cli repl --dep org.typelevel::cats-core:2.9.0 --dep org.typelevel::kittens:3.1.0                                                                                                                                                                                                                                                 ◀╼╢pts/1╟╾╯
scala> import cats.*, cats.implicits.given, cats.derived.*

scala> case class Foo(a: Int) derives Show
// defined case class Foo

scala> class Bar(b: Int) derives Show
-- [E172] Type Error: ----------------------------------------------------------
 1 |class Bar(b: Int) derives Show
   |                          ^
   |No given instance of type cats.derived.DerivedShow[Bar] was found.
   |I found:
   |
   |    cats.derived.DerivedShow.given_DerivedShow_A[Bar](
   |      shapeless3.deriving.K0.mkProductInstances[
   |        [A] =>> cats.derived.Derived.Or[cats.Show[A]], Bar](
   |        /* missing */summon[shapeless3.deriving.K0.ProductGeneric[Bar]]),
   |    ???)
   |
   |But Failed to synthesize an instance of type shapeless3.deriving.K0.ProductGeneric[Bar]: class Bar is not a generic product because it is not a case class.
   |----------------------------------------------------------------------------
   |Inline stack trace
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from DerivedShow.scala:19
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from DerivedShow.scala:19
    ----------------------------------------------------------------------------
1 error found
2 Likes

What kind of features would you like to have without which restrictions?

1 Like

For one, I might want some of the features, but not others, without having to explicitly disable/override/ignore them.

Additionally, case classes

  1. cannot extend other case classes.
  2. can only take vals (no call-by-name or lazy parameters), so they cannot be used for implicit product type representations.
  3. cannot have dependencies between parameters.

Have I missed any other restrictions? It does look like I exaggerated when I wrote that they “impose a lot of restrictions”…

Could all the case class conveniences be derived for custom Product types?

Possibly, but some of them would often be wrong. For example, the hashcode implementation assumes that this is an immutable data structure. And IIRC the restriction on case classes inheriting from other case classes is because getting equality right is difficult in the face of inheritance.

1 Like

and vars

1 Like

And IIRC the restriction on case classes inheriting from other case classes is because getting equality right is difficult in the face of inheritance.

Exactly. That restriction comes from opting into the whole package of case classes, which includes equality.

the hashcode implementation assumes that this is an immutable data structure.

A typical implicit product type (where the elements are computed rather than stored) would be immutable, so there wouldn’t be any issue there.

Sure, but in making these things truly modular you’re talking about opening the door much wider than that. (At least, that’s the way I interpret your suggestion.) Odds are pretty good that these features would get heavily misused by people who don’t understand their limitations and when they are and aren’t appropriate. So it’s a bit of a Pandora’s Box.

(As it is, I tend to think that the fact that you can put vars into case classes, as Seth points out, is arguably a misfeature that causes more harm than good.)

2 Likes