Hi folks,
I’m a heavy user of scala-newtype, which allows you to have zero-cost wrappers in Scala 2. For those unfamiliar with it, this is how one can define a newtype:
object data {
@newtype case class Foo(value: String)
}
This library is built with macros so it is not available in Scala 3. However, that should not be a problem since we have opaque types.
Current status
After trying to migrate a library that uses newtype to cross-compile to Scala 3, I found that opaque types are bit cumbersome in comparison. Here’s the amount of code I need to write to get more or less something equivalent in Scala 3.
object data:
opaque type Foo = String
object Foo:
def apply(v: String): Foo = v
end Foo
extension (x: Foo) {
def value: String = x
}
end data
But it does not end here. Another bigger issue pops up when we introduce more newtypes and want to access the underlying primitive - String
in this case.
object data:
opaque type Bar = String
object Bar:
def apply(v: String): Bar = v
end Bar
opaque type Foo = String
object Foo:
def apply(v: String): Foo = v
end Foo
extension (x: Bar) {
def value: String = x
}
extension (x: Foo) {
def value: String = x
}
end data
This fails to compile with the following error.
Double definition:
def value(x: data.Bar): String in object data at line 13 and
def value(x: data.Foo): String in object data at line 17
have the same type after erasure.
Consider adding a @targetName annotation to one of the conflicting definitions
for disambiguation.
If we follow the compiler’s suggestion, we can get it working by adding a @targetName
to each extension method.
extension (x: Bar) {
@targetName("value_Bar")
def value: String = x
}
extension (x: Foo) {
@targetName("value_Foo")
def value: String = x
}
So this works, albeit involving quite a lot of boilerplate.
Summarizing, it takes as much as 18 lines of Scala 3 code and only 4 lines of Scala 2 code (using the scala-newtype
library) to define two newtypes.
Proposal
There are two main things I would like to propose, though, I’m not sure how feasible they are since I’m not very experienced on the Scala compiler side.
Auto-generate wrapping / unwrapping functions
For comparison, here’s all the necessary code to define a newtype in Haskell:
newtype Foo = Foo String
A function String -> Foo
- i.e. apply
in Scala - will be automatically generated for us. It is also possible to define a function that goes the other way around: Foo -> String
, aka the unwrapping function, if we define it as follows.
newtype Foo = Foo { getFoo: String }
Creating both the wrapping and unwrapping functions is something that the scala-newtype
does by default in Scala 2.
@newtype case class Foo(getFoo: String)
In my opinion, this greatly improves the user experience, and it would be great if we can get this feature with opaque types in Scala 3.
Generic extension methods
This is more a nice-to-have and would not probably be necessary for the main use case (unwrapping) if we get opaque types with unwrapping functions by default. So, assuming that unwrapping functions do not exist, would it be possible to define a generic extension method for any opaque type?
e.g. a generic unwrap
function (please, assume this compiles, I don’t know exactly what the equivalent of asking for an implicit ev: OpaqueTypeOf[A, B]
is in Scala 3).
extension[A] (x: A) {
def value[B](given ev: OpaqueTypeOf[A, B]): B = x
}
Moreover, it would be useful to define generic typeclass instances for opaque types. Though, I get the feeling this is probably possible in Scala 3 using typeclass derivation?
In Scala 2, we can do the following with scala-newtype
.
@newtype case class Foo(value: String)
object Foo {
implicit val eq: Eq[Foo] = deriving
}
If there’s an Eq[String]
in scope, the Eq[Foo]
can be derived. There’s also an oficially non-recommended typeclass named Coercible
, which looks similar to the OpaqueTypeOf[A, B]
I made up above. By exploiting this typeclass, we can derive typeclass instances generically for any newtype. I do - and recommend - this all the time to avoid writing boilerplate. Here’s an example.
import cats.Eq
import cats.implicits._
import io.estatico.newtype.Coercible
object eq {
implicit def coercibleEq[A: Coercible[B, *], B: Eq]: Eq[A] =
Eq[B].contramap[A](_.asInstanceOf[B])
}
However, it would be much better if we could use typeclass derivation for opaque types. Assuming there are instances of Eq[String]
and Show[String]
in scope, would something like this be possible?
opaque type Foo = String derives (Eq, Show)
That’s all I’ve got for now. Please forgive my ignorance if something does not make sense, I’m a newbie to Scala 3
I would love to hear your thoughts.