Proposal for Opaque Type Aliases

#81

At least for collections it does:

(List(Ipv4.fromBits(1)): Any) match {
  case 1 :: tail => 
}
#82

This is the bug right there, not where we are consuming Any and matching, but where we are converting to Any. Anywhere an opaque type is cast should be a compiler error or at minimum warning. I would say it should be an error unless annotated or otherwise allowed.

Alternatively, the libary could have some sort of designation at the definition site to allow or disallow erasing the opaque type.

Its not the definition of receive that is an issue, it is send – it is clearly an error to send an opaque type. If you want to only send an int and serialize an int, wrap it in a case class or have a method on it that extracts the Int and send the Int, so the intention is clear.

I propose that upcasting to Any is a compiler error – after all, opaque types are purely compiler side guarantees, and correctness is broken when you erase them. If you want to send the underlying type, you must explicitly do so. Implicit erasure of an opaque type is evil.

Likewise, could one completely disallow matching on an opaque type? Matching an explicit value is ok, matching the type only doesn’t make sense to me.

case i: Ipv4 => – disallow, compiler can not prove it is not any other sort of Int
case i if i == Ipv4.fromString("127.0.0.1").asInt => – ok, we are just checking Int now
case i if i == Ipv4.fromString("127.0.0.1") => – maybe disallow?

1 Like
#83

Note, this same issue exists with path dependent types:

abstract class Foo {
  type Bar
  def makeBar: Bar
  def takeBar(b: Bar): Int
}

class Foo1 extends Foo {
  type Bar = Int
  def makeBar = Int.MaxValue
  def takeBar(b: Bar): Int = b
}

val foo: Foo = new Foo1
val bar: foo.Bar = foo.makeBar
bar match {
  case i: Int => println("it's an int!")
  case _ => println("it's not an int")
}

That code compiles without warning for me in scala 2.12.8. So, the kind of problem you are talking about already exists, and the problem is expecting all compile time type information to exist at runtime with pattern matches, which it already does not.

opaque type is for use cases where you want stricter types at compile time, but without introducing any runtime change. You are showing examples where you explicitly do want a runtime change because you want runtime pattern matching to behave differently. That is in conflict of the goal of the feature. If you need runtime type information, I think you need to introduce a new class, or your own custom tag.

4 Likes
#84

This is already pretty bad code right now…
You simply shouldn’t send values like this to an akka actor. You should introduce custom messages like case class Increment(i: Int) and case class Ping(ip: Ipv4).

#85

I might say warning instead of error, but this is an interesting point. I have no idea whether it’s actually feasible – since Any is kind of the universal get-out-of-jail-free card, I don’t know whether it would be straightforward in the compiler to prevent this upcast, and from a type-theory perspective it seems weird to have a type that isn’t just an ordinary subtype of Any.

But it does seem like it goes against the intent of opaque types to allow this upcast silently – requiring an explicit : Any if you really want to do the upcast seems like it might nudge people away from this particular bad idea…

1 Like
#86

I think that this is a good idea but note that it won’t be perfect: you are going to have to choose between false positives and false negatives. Consider a method toAny:

def toAny[T](t: T): Any = t: Any

Should defining toAny trigger errors? I would say “no” because it’s well-formed and should be valid in a language with Any.

Should calling it on opaque types trigger errors? I would say “no” because, from the call site’s point of view, it consumes the opaque type without upcasting it and just happens to return an Any.

#87

Rather than blocking pattern matches involving opaque types completely and preventing stylistically-useful code, what about restricting them down a bit: maybe opaque types should only be allowed to appear in pattern matches on types that are made up of unions of themselves and other things with different erasures (including the degenerate union of just the opaque type itself)?

For example, suppose you had the following opaque types:

opaque type UserId = UUID
opaque type ProductId = UUID
opaque type EmailAddress = String
object EmailAddress {
  def unapply(e: EmailAddress): Option[String] = Some(e)
}

Then you could write:

(???: Int | UserId | EmailAddress) match {
  case i: Int => "Int"
  case u: UserId => "UserId"
  case e: EmailAddress => "EmailAddress"
}

val EmailAddress(str) = (???: EmailAddress)

(???: EmailAddress) match {
  case EmailAddress(str) if str.endsWith(".com") => ".com address"
  case _ => "other address"
}

but not:

(???: UserId | ProductId) match {
  case u: UserId => "UserId"
  case p: ProductId => "ProductId"
} // error: same erasure

(???: EmailAddress | String) match {
  case e: EmailAddress => "Email"
  case _ => "Something else"
} // still error: same erasure

(???: Any) match {
  case EmailAddress(str) => s"Email: $str"
  case _ => "Something else"
} // error: match performs unsafe cast

This would not get rid of the possibility to “forget” that a runtime value has existed as an opaque type by upcasting to Any and then matching on the erasure, but it should prevent something from being silently and unsafely cast to be an opaque type (which is likely to cause intended invariants to be violated) by upcasting something with the same erasure to Any and then matching on the opaque type.

Edit just to make it absolutely clear: my pitch is that when any opaque type appears in the pattern for any case, you do an analysis of the type being matched on, not that you somehow do some analysis of all the types appearing in all the cases, which I think would be very tough to get right.

#88

This allows you to pass in the erasure of the opaque type and violate its invariants. For example, let opaque type Nat = Int. Its own methods will forbid negative numbers, but pattern matching would allow you to have a negative natural number.

1 Like
#89

Ah no, I’m saying that you wouldn’t be able to do a match where the left-hand side has the same erasure as an opaque type appearing in any of the casees. For example, in your case, if Nat appeared in any of the cases and the type of the left-hand side was Int or anything with Int as its erasure other than Nat, there would be an error.