scala-newtype icon indicating copy to clipboard operation
scala-newtype copied to clipboard

Type Roles

Open carymrobbins opened this issue 6 years ago • 1 comments

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

carymrobbins avatar May 31 '18 20:05 carymrobbins

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

amilkov3 avatar Jul 12 '18 07:07 amilkov3