Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type Roles #29

Open
carymrobbins opened this issue May 31, 2018 · 1 comment
Open

Type Roles #29

carymrobbins opened this issue May 31, 2018 · 1 comment

Comments

@carymrobbins
Copy link
Member

carymrobbins commented May 31, 2018

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 -

@amilkov3
Copy link

amilkov3 commented Jul 12, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants