Need an automated way to generate derived TypeClasses (e.g. for Quill)

Dotty’s TypeClass derivation mechanism is really great! I’m really excited about it because it can replace many internal Quill mechanisms in an idiomatic way. The problem is, as it stands today, it is necessary to manually “register” every single GADT with a type-class before the type-class can be used. In Quill that would mean something like this would have to be done with every single user’s record-class:

case class Person(name: String, age: Int)

given Encoder[Person] = Encoder.derived
given Decoder[Person] = Decoder.derived
given Expander[Person] = Expander.derived
given Meta[Person] = Meta.derived

// Only then can we do this:
inline def v = quote {
  query[Person].filter(p => p.name == "Joe")
}

I think this is waaay too much ceremony required on a user. Especially for things like Expander and Meta which are implementation details which are not supposed to be even known by users (i.e. we could decide to change them in a minor-version!). Even for the Encoder/Decoder stack however, I don’t think this is something a user shouldn’t even have to know about unless they are using custom datatypes.

There needs to be some way to automatically register these type-classes for some record class (e.g. Person) when something like query[Person] is called. I tried doing something like this inside of a macro by using mirrors summoned via summonExpr as well summonFrom (see #7853 and #7849 respectively) but it currently doesn’t work. Can we get these issues fixed or have a more flexible mechanism to register type-classes with Scala 3?

2 Likes

Why isn’t it possible to define some type class Quill[A], which users would derive instances for, and from which Quill could provide the given Encoder, Decoder, Expander, and Meta instances?

// Quill
trait Quill[A] { ... }

given quillEncoder[A](given quill: Quill[A]): Encoder[A] = ...
given quillDecoder[A](given quill: Quill[A]): Decoder[A] = ...

// User
case class Person(name: String, age: Int)
given Quill[Person] = Quill.derived

inline def v = quote { query[Person].filter(...) }

This does not solve the problem of compatibility across quill’s versions, though…

4 Likes

You can use

case class Person(name: String, age: Int) derives Encoder, Decoder, Expander, Meta

as an abbreviation for the given instances.

@julienrf That’s a neat trick that could make things a bit more palletable at the cost of portability.

@odersky Expander and Meta are implementation details related to Quill’s inner AST. Forcing a user to be aware of them is a major abstraction leakage.

1 Like

More than an abstraction leakage, it creates a dependency problem. I usually want my data model case classes in a module that is cross-compiled and does not have a dependency on the database library.

It’s true that the core of Quill can be used in cross-compiled code, but I don’t want it to be a dependency since it’s not really used there. It’s not the end of the world.

Another option might be to use something like SchemaZ, which I believe Valentin Kaas gave a recent talk about.

2 Likes

Can’t you let the user just do a derives Quill and do the rest internally?

6 Likes

That’s reasonable. Going to try it.

It would be interesting to see more design patterns for Typeclasses like this.

4 Likes

Sorry it was Kasas. https://www.youtube.com/watch?v=wfQDmI8xEEg

Wait a minute, you don’t actually need to do derived Typeclass or given Typeclass[MyClass] for anything! Have a look at my example in #7875.
This should be documented!

Btw, thank you @julienrf for the additional info! In Quill I want to give users the option to do auto as well as semi-auto derivation. It should be documented that both patterns are possible.