Ergonomics of configurable derivation

Currently configurable type class derivation can be achieved by requiring extra implicit arguments to the derived method. Example (from Circe Derivation PR):

object ConfiguredEncoder:
  def derived[A](using c: Configuration)(using m: Mirror.Of[A]): ConfiguredEncoder[A] = ???

Usage:

  1. Using a global configuration for every derivation:
// Somewhere in the global implicit scope
given Configuration = Configuration.default.withSnakeCaseNames

case class Foo(s: String = "bar", i: Int) derives ConfiguredEncoder
  1. Using a specific configuration (this also overrides any configuration that might exist in the global scope):
object Bar:
  given Configuration = Configuration.default.withDiscriminator("_type")
case class Foo(s: String = "bar", i: Int) derives ConfiguredEncoder

This could be improved in the following ways:

  1. The derives syntax could behave like with, that is, this would be valid:
case class Foo(s: String = "bar", i: Int)
  derives ConfiguredEncoder
  derives ConfiguredDecoder
  derives Eq
  derives Show

Just like with the extends either all type class derivations must use the derives keyword or all must use the comma separated list (eg: derives ConfiguredEncoder, ConfiguredDecoder, Eq, Show)

  1. When, and only when, using the above syntax this would be valid:
case class Foo(s: String = "bar", i: Int)
  derives ConfiguredEncoder(using Configuration.default.withDiscriminator("_type"))

Making it succinct and to the point. Even more so if multiple vals with distinct configurations are declared globally. For example:

case class Foo(s: String = "bar", i: Int)
  derives ConfiguredEncoder(using discriminatorConf)
  1. The derives method could accept non implicit arguments:
object ConfiguredEncoder:
  def derived[A](
    transformMemberNames: String => String = identity,
    transformConstructorNames: String => String = identity,
    useDefaults: Boolean = true,
    discriminator: Option[String] = None,
    strictDecoding: Boolean = false,
  )(using m: Mirror.Of[A]): ConfiguredEncoder[A] = ???

If all non-implicit arguments define default values then the comma separated list syntax can be used. Otherwise the derives keyword per derivation must be used passing in the intended values. This would allow usages like:

case class Foo(s: String = "bar", i: Int)
  derives ConfiguredEncoder(useDefaults = false, discriminator = Some("is_a"))
4 Likes

Hello!

According to the specification, this should be supported. If that doesn’t work, could you please open an issue in GitHub - lampepfl/dotty: The Scala 3 compiler, also known as Dotty.?

Your two other suggestions look reasonable to me, although I wonder if we should put a lot of effort into supporting variations around the derives syntax when you can always fallback to a manual instance definition:

object Foo:
  given Encoder[Foo] = ConfiguredEncoder(...)(using ...)
2 Likes

I wanted to mention the with syntax and not the extends syntax, sorry for the confusion (edited the main post, to reduce further confusion).

The comma separated list syntax works. I’m suggesting that this should also work:

case class C(a: Int, b: String = "")
  derives Encoder
  derives Decoder

I don’t get why you insist on only allowing syntax like

class Baz derives Typeclass1(1) derives Typeclass2(2)

and not

class Baz derives Typeclass1(1), Typeclass2(2)

as for inheritance you can have both

class Baz extends Foo(1) with Bar(2)

and

class Baz extends Foo(1), Bar(2)
4 Likes

I’m insisting because classes with multiple derivations each with multiple configurations would be less legible under comma separated syntax:

case class For(i: Int, s: String) derives ConfiguredEncoder(_.toUppercase, _.toLowercase, discriminator = Some("_type")), ConfiguredDecoder(_.toUppercase, _.toLowercase, useDefaults = false, discriminator = Some("_type"), strictDecoding  = true)

vs

case class For(i: Int, s: String)
  derives ConfiguredEncoder(_.toUppercase, _.toLowercase, discriminator = Some("_type"))
  derives ConfiguredDecoder(_.toUppercase, _.toLowercase, useDefaults = false, discriminator = Some("_type"), strictDecoding  = true)

I see no problem in supporting both syntaxes.

2 Likes

Legibility is quite subjective and hard to measure in general but for me it feels very much in the spirit of scala to give programmers a possibility to be as explicit as it feels necessary to preserve legibility in a given context but more implicit and concise if things seem to be simple and obvious.
So personally I would be OK with both commas as separators and repeated derives (although I’m not sure if we should allow mixing the two syntaxes, like class Foo derives Encoder, Decoder derives Show)

3 Likes

Completely agree, especially the mixing.