Allow renames of givens generated by 'derives' clauses

Currently whenever we use a derives clause to generate a typeclass instance its name in the companion object is generated as ‘derived$NameOfTheTypeclassType’ eg. for a typeclass like Show it’d be derived$Show.

I propose to extend the syntax of ‘derives’ with an optional ‘as name’ clause, for example:

trait Show[A]

object Show {
  def derived[A]: Show[A] = ???
}

case class Person(name: String) derives Show as show

Person.show // I can now access 'show' by using its name as opposed to derived$Show

Ok, but why?

I’ve found myself implementing typeclasses that function as isomorphism between types and that provide methods to convert between those types (which I’d like to expose as constructors on the companion object of the type that derives a given typeclass), let’s take a typeclass called YesNoEnum that captures the isomorphism between enums with cases of Yes and No and Booleans:

trait YesNoEnum[A] {
  extension (self: A) def toBoolean: Boolean

  def apply(bool: Boolean): A
}

object YesNoEnum {
  inline def derived[A <: scala.reflect.Enum](using
    A: Mirror.SumOf[A],
    ev1: A.MirroredElemLabels <:< ("Yes", "No")
  ): YesNoEnum[A] = ???
}

…and an example Yes/No enum that derives our typeclass:

enum Decision derives YesNoEnum {
 case Yes, No
}

So, currently to get my desired behavior (of exposing the apply method from YesNoEnum in the companion) I’d have to do this:

Current state of the art

object Decision {
  export derived$YesNoEnum.{apply as fromBoolean}
}

Which, to be honest, is not that bad! But just seeing the $ in the generated name is enough to make me think this is compiler guts spilling over to my code (+ there’s a weird issue with Metals where the entirety of derived$YesNoEnum doesn’t get autocompleted, it only autocompletes up to derived even though intellisense shows the full name, but I digress)

Another alternative is to NOT use derives and call the derived method by hand:

object Decision {
  given fromBoolean: YesNoEnum[Decision] = YesNoEnum.derived
}

…which is fine enough as well… I guess… But just typing out the whole type ascription is enough to make me not like it as much (since we’re ever so close to perfection!)

The proposal in action

So with my proposal in mind the only thing we’d have to change is adding an as clause to our derives clause, like so:

enum Decision derives YesNoEnum as fromBoolean {
 case Yes, No
}

…and now we can actually call

Decision.fromBoolean(true) // imagine it evaluates to Decision.Yes

without even needing to define a companion object!

POC

I’ve implemented a POC of my proposal here with YesNoEnum actually working the same way I described above.

If there’s actual buy in for something like this I’d like to draft a SIP and spearhead an implementation of it :blush:

4 Likes

Very nice !

This seems perfectly in line with the current use of “as”

I don’t have a lot of experience with typeclasses yet, so I can’t comment on how useful it would be, but your example seems convincing

If I remember correctly, you are absolutely right, identifiers with $ in them are invalid (when in source-code), there is just no warning or error (not sure why), and the compilers plays ball with them … because they are the ones it uses internally

IIRC -Wunused relies on the name, as happens with other members such as default arg methods.

I would second this for another reason:

in general you can package “static” and extension methods together in a single type class, e.g. for a bitset

trait Flags[T]:
  outer =>

  opaque type FlagSet = Long
  val EmptyFlagSet: FlagSet = 0L
  
  object FlagSet:
    extension (flags: FlagSet)
      def isEmpty: Boolean = flags == EmptyFlagSet
      def |(flag: A): FlagSet = flags | flag.toFlags
      def is(flag: A): Boolean = (flags & flag.toFlags) != EmptyFlagSet

  extension (t: T) def toFlags: FlagSet

  object companion:
    export outer.{FlagSet, EmptyFlags}
end Flags

// EDIT: added companion
object Flags:
  inline def derived[T]: Flags[T] = ??? // some macro/inline that implements `toFlags`

then you might want the companion of the deriving object to receive the “static” methods:

enum Mode derives Flags as flagset:
  case InMethod, InParents, InAnnotation

object Mode:
  export flagset.companion.*

// other code
def foo = Mode.EmptyFlags | Mode.InMethod

This proposal would make that use case more ergonomic, although perhaps more magical types (e.g. an extension method on T.companion) could be introduced so there’s no need for export. (again though stable paths are desirable)

Trying to grok your code… Where is the abstract toFlags implemented? (And I guess EmptyFlagSet should be EmptyFlags?)

You’re right, I have renamed to EmptyFlagSet.

Otherwise I didn’t include the code, but the Flags.derived method (which I previously left out, now edited in) would implement the toFlags method (e.g. with 1 << ordinal for enum values)

Thnks! I understand better now.