scala-newtype
scala-newtype copied to clipboard
Type Roles
Currently, for newtypes we generate a Coercible[O, N]
instance where O
is the original type and N
is the newtype. While this is perfectly fine, we go a step further and generate a Coercible[F[O], F[N]]
instance for any F[_]
. This is where we get into trouble. There are cases where this shouldn't be permitted due to the nature of the data structure we're dealing with. For example, let's look at how this would work with dogs.Set which relies on an implicit Order
instance for its operations -
import cats.instances.int._
import cats.Order
import io.estatico.newtype.ops._
import io.estatico.newtype.macros.newtype
// Like Int except ordered in reverse.
@newtype case class RevInt(value: Int)
object RevInt {
implicit val order: Order[RevInt] =
Order.from((x, y) => -Order[Int].compare(x.value, y.value))
}
// Build a dogs.Set[Int]
println(dogs.Set(1, 2, 3))
// Set(1,2,3)
// Build a dogs.Set[RevInt]
println(dogs.Set(RevInt(1), RevInt(2), RevInt(3)))
// Set(3,2,1)
// Build a dogs.Set[Int], coerce it to dogs.Set[RevInt], and add an element
println(dogs.Set(1, 2).coerce[dogs.Set[RevInt]] + RevInt(3))
// Set(3,1,2)
One quick and dirty way to deal with this would be to introduce type roles via a type class.
trait TypeRole[A] {
type Role
}
object TypeRole {
def mk[A, R]: TypeRole[A] { type Role = R } =
_instance.asInstanceOf[TypeRole[A] { type Role = R }]
private val _instance = new TypeRole[Nothing] {}
type Nominal[A] = TypeRole[A] { type Role = types.Nominal }
type Representational[A] = TypeRole[A] { type Role = types.Representational }
object types {
sealed trait Representational
sealed trait Nominal
}
}
Then we'd define this Coercible instance based on the type role -
implicit def reprF[F[_], A, B](
implicit ev: TypeRole.Representational[F[A]]
): Coercible[F[A], F[B]] = Coercible.unsafe
We then need to define type role instances -
implicit def typeRoleScalaSet[A]: TypeRole.Representational[Set[A]] = TypeRole.mk
implicit def typeRoleDogSet[A]: TypeRole.Nominal[Set[A]] = TypeRole.mk
// Compiles
println(Set(1, 2, 3).coerce[Set[RevInt]])
// Set(1, 2, 3)
// Does not compile now
println(dogs.Set(1, 2, 3).coerce[dogs.Set[RevInt]])
See -
- https://ghc.haskell.org/trac/ghc/wiki/Roles
- https://stackoverflow.com/questions/49209788/simplest-examples-demonstrating-the-need-for-nominal-type-role-in-haskell
macro? should be a little less quick, more dirty, but you're sweeping the dirt under the rug. although im not sure if it'll be a better (or worse i guess?) maid than what you have there. but it might upgrade you to an old/quite poor vacuum cleaner at least i.e.: it can generate some of that boilerplate you have if this or some variation is an acceptable encoding for this which it is as far as i can tell.
some thing to consider for posterities sake is how a higher kinded Coercible
in general would play with partial unification related things, like if you have an Either
you want to type lambda to a kind F[_]
you'd have to probably use type projector? in which case, if you went down the compiler plugin road, itd might have to integrate with it, lets say. this is super far down the line concern though