Could Someone Give me Advice on Best Practices for Typeclass Derivation in Scala 3?

Hello there,

I have been diving deeper into Scala 3 and exploring its new features, particularly typeclass derivation. While I understand the basic concept of derives and how it simplifies boilerplate for common use cases like JSON serialization, equality checks, etc.; I have encountered a few scenarios where I am unsure about the best practices.

I understand that Scala 3 supports both automatic and semi-automatic derivation. Automatic derivation is convenient; but I have read concerns about potential performance overhead or accidental derivations leading to unexpected behavior.

For larger projects; do contributors generally recommend using semi automatic derivation for better control, or is automatic derivation sufficient in most cases?

Suppose I am working with a typeclass like Show akin to string formatting. If a library already provides a default derivation; but I need to customize the behavior for specific case classes; what is the recommended way to override or extend those defaults?

Finally; I would love to hear examples or insights from real world projects where typeclass derivation in Scala 3 has significantly improved maintainability or reduced boilerplate. Are there common pitfalls or design patterns contributors here have learned to avoid?

Also, I have gone through this post; https://contributors.scala-lang.org/t/what-kinds-of-macros-should-scala-3-support-cybersecurity which definitely helped me out a lot.

I want to ensure I am leveraging Scala 3’s new typeclass features effectively and in line with community standards.

Thanks in advance for your help and assistance.

Disclaimer: All large projects that I worked in were in Scala 2, so I’ll only be talking about that, but I think the same applies to Scala 3:

I understand that Scala 3 supports both automatic and semi-automatic derivation. Automatic derivation is convenient; but I have read concerns about potential performance overhead or accidental derivations leading to unexpected behavior.

If it’s not a small script or PoC, just use semi-automatic derivation. It’s not that much harder to use and it avoids a lot of issues down the line:

  • The compilation is faster
  • It’s easy to ensure that your overrides are respected
  • There are no implementations for things that shouldn’t have one (e.g. you might really not want a Show[Password]).

Suppose I am working with a typeclass like Show akin to string formatting. If a library already provides a default derivation; but I need to customize the behavior for specific case classes; what is the recommended way to override or extend those defaults?

Again, with semi-auto derivation, just provide the Show instances that you want in the companion object of the case classes and you should have no issues (in general, this should also work with automatic derivation but… it’s a bit easier to mess up).

Finally; I would love to hear examples or insights from real world projects where typeclass derivation in Scala 3 has significantly improved maintainability or reduced boilerplate. Are there common pitfalls or design patterns contributors here have learned to avoid?

Not 100% Scala 3, but Pureconfig is a blessing IMO.
It’s just so easy to mess up config reading and parsing, that having a library that:

  • Ensures that you put everything in a well typed class
  • Load everything and validates it at the start of the app
  • Without having to write any boilerplate

Just reduces a huge surface of errors of “the application was deployed with a wrong config and didn’t fail fast, so it crashed at midnight when a scheduled task was running”.

1 Like

I use and write a reasonable amount of derived Typclasses in Scala 3.
My primary usecases are replicated data types, deriving typeclasses for merging (lattices), bottom values, and state decomposition.

For those, making derivation of typeclasse explicit (what is sometimes called semi-automatic) was the obvious choice:
• Implementation is straightforward, the derivation just computes a single step of the composition.
• having a given Lattice[MyType] = Lattice.derived is good documentation that MyType should be replicatable
• You never have to do it for that many types, that writing the above would become cumbersome.

On the other hand, I think jsoniter-scala has the correct tradeoff, with being able to derive instances for more complex types, without the intermediary declaration, because the nature of JSON codecs is that you have many deeply nested types, often in an ad hoc manner (one for every different API).

Though, do note, that it is relatively straightforward to just offer both, your derivation simply needs a switch that potentially calls itself recursively.

I would strongly recommend against any approach where generic givens are derived, thus regenerating the instances on use, this seems like a complete antipattern to me.


For your general about typeclass derivation with Scala 3: I don’t think this is strictly about type classes. It’s just the ability to write generic code over any product or sum type using Type based mirrors is a good enabler of generic code, without requiring more complex forms of meta programming.
Though, in my experience using any of the generic Tuple stuff is often best used by just checking that the types match up in your API (i.e., that two tuples do have the same types at each position) and then just ignoring all types during the implementation. The type preserving API on tuples seem to produce completely unreadable code, and it seems much more reliable to just use manual reasoning and tests to ensure your internal types are correct.
This is also what the Tuple API does internally, because the type system does not really understand any of the relations between these types at all.

1 Like