`import Foo derives Ordering`

Currently derives keyword is allowed only at the definition site of a type, like:

enum Tree[T] derives Eq, Show {
  case Branch[T](left: Tree[T], right: Tree[T])
  case Leaf[T](elem: T)
}

The enum above doesn’t derive Ordering typeclass. But at the call site, we can still derive it via explicitly calling Ordering.derived, like:

import Tree
given [T: Ordering] as Ordering[Tree] = Ordering.derived

I propose reusing the derives keyword to do the above in one line that is consistent with the definition site syntax:

import Tree derives Ordering

Now to take it further, we can also support this at the type alias level, like:

package improvedModel
type Tree = model.Tree derives Ordering

This will allow library designers to write core modules that define the enums without depending on libraries that define typeclasses (like Cats), then define a separate module that defines alternative imports for Cats users.

What do you think?

1 Like

While I can see the appeal, it feels wrong to me.

The thing is (if I understand correctly), derives is actually generating code, and not a small amount of it. That doesn’t seem appropriate in either of these circumstances. import just brings existing names into scope – it doesn’t really create anything. And type aliases are pretty similar: they are creating a new name that points to an existing type, but AFAIK don’t generate much by way of runtime code.

I agree that being able to easily derive typeclass instances externally in a way that is more consistent with the derives syntax would be nice, but neither of these seems appropriate offhand. (Possibly someone on the team will disagree with me?)

7 Likes

Sounds to me that we already have the perfect candidate, then:

export mypackage.Tree deriving Ordering

Indeed, export is already expected to generate code — forwarders. Why not allow it t generates instances along with them?

However, I don’t think the newly-derived instances would be in the implicit scope, so people likely will have to import them later.

4 Likes

So export instead of type alias syntax, not import. Right?

I share @jducoeur concern. derives generates lots of code, which slows down compilation and increases footprint (*). That’s OK if we generate the code once with the deriving class. But it can quickly become problematic if it’s done at use site. So using the same syntax for both creates a dangerous equivalence.

(*) This is no small matter: I believe the primary driver for Scala’s reputedly slow compilation is exactly this: code avalanches created by uncontrolled typeclass derivations.

1 Like

They would be added to the implicit scope if the alias is defined using bounds:

type Tree >: mypackage.Tree <: mypackage.Tree
given Ordering[Tree]

We’ve talked about this before, although I haven’t used this in real-life and can’t vouch that the alias can’t possibly be lost at some point (it shouldn’t, but it also does unify with mypackage.Tree so who knows)

They can also be added to the implicit scope using overloading:

type Tree = mypackage.Tree
given Tree as Ordering[Tree] = ...

But this will only work when Tree is imported directly, if it’s in scope indirectly (e.g. through a trait) the implicit won’t be visible. I call this “implicit punning” and use it constantly, BIO bifunctor typeclass hierarchy is 100% built on this interaction and another tagless final library tofu has added syntax using it recently as well.

4 Likes