Pre-SIP derives clauses for opaque types

Pre-SIP derives clauses for opaque types

I propose two changes to the Scala language:

  1. Adding a derives clause for opaque types that will be desugared to a normal call to the derived method on the typeclass.
    e.g.

    opaque type USD = Int derives Show
    

    will be desugared to

    opaque type USD = Int
    object USD {
      given Show[USD] = Show.derived[USD]
    }
    

    I think that this use case might not be very popular, especially since there are no Mirrors for opaque types. But they still can be generated using macros, so it’s the only reasonable thing to keep it consistent with classes.

  2. Adding a derives via clause for opaque types that will be desugared to a proxy to another types instance.
    e.g.

    opaque type USD = Int derives Show via Int
    

    will be desugared to

    opaque type USD = Int
    object USD {
      given (using intShow: Show[Int]): Show[USD] = intShow.asInstanceOf[Show[USD]] // obviously try to avoid the `asInstanceOf` if possible
    }
    

    I think that this might be by far the most useful case, since it is often that case that the instance for the underlying type can be used for the opaque type.

    When it comes to syntax, the via clause will have to be used per typeclass, so the following two declarations will NOT be equivalent.

    opaque type USD = Int
      derives Eq, Show via Int
    // and
    opaque type USD = Int
      derives Eq via Int, Show via Int
    

Some variations

The downside of this syntax is that it introduces a new keyword. And although via shouldn’t be a popular symbol name, this change won’t nearly be common enough to justify introducing a new keyword.

All syntax option to consider:

  • derives Show via Int
  • derives Show using Int
  • derives Show as Int

Another notion to consider is the desugaring of derives via clauses. Two possibilities:

  1. given (using intShow: Show[Int]): Show[USD] = intShow.asInstanceOf[Show[USD]]
    
  2. given Show[USD] = Show.derived[Int].asInstanceOf[Show[USD]]
    

Any feedback greatly appreciated.

6 Likes

Personally I have some doubts regarding the position of derives clauses. To me it would feel more consistent to put them on the left side of the definition, e.g.

opaque type USD derives Show = Int

as this influences the type alias itself rather than it’s implementation, similarly to how we place type bounds, e.g

opaque type USD <: Currency = Int

On the other hand with might look weird when we derive a lot of type classes, e.g.

opaque type USD
  derives Foo1
  derives Foo2
  derives Foo3 = Int

There was also a proposal allowing to pass extra parameters to derives clauses explicitly Ergonomics of configurable derivation. I’m wondering if these two features could work well together.

3 Likes

I am not sure about the advantage gained by the second construct, the derives via. If I understand correctly, in the given snippets

opaque type USD = Int derives Show via Int

Is effectively equivalent to:

opaque type USD = Int derives Show

As it is in the scope where the opaque type is defined. What would be the use case for it?

Edit: After explanation from the author I see that there is important difference as the derives via does not invoke derived, but instead “forwards” the given. Nevertheless, given how derives syntax works, I think that a lot of people would assume that
opaque type USD = Int derives Show via Int
Would mean:

opaque type USD = Int
object USD {
  given Show[USD] = Show.derived[Int]
}
2 Likes

I couldn’t explain it, but for me
opaque type USD derives Show = Int
looks weirder than
opaque type USD = Int derives Show

I think it also has the advantage of going well with bounds:
opaque type USD <: Number = Int derives Show
vs
opaque type USD derives Show <: Number = Int
or
opaque type USD <: Number derives Show = Int

I agree that the via mechanism is a bit unexpected, but if it behaves intuitively enough for users not having to worry about it, it is not such a concern

2 Likes