Proposal for Opaque Type Aliases

I suppose you can still use standard Java reflection, right?

Okay, cool. For reference, the case I desperately need (with no way to work around without an almost insuperable amount of work) is actually ClassTag – specifically, being able to get at the runtime identity of the Class in such a way that it can be used as a key in a Map. My DI relies on this.

The relevant code is here. I’m fine with changing the implementation, and don’t care about ClassTag per se; what I care about is that there be some sort of typeclass that allows similar functionality, preferably without having to change the (many hundreds of) call sites.

ClassTag already works with Dotty.

1 Like

… and there are no changes planned.

1 Like

If this is intended to run on the JVM, I hope you aren’t putting a class as a key in a map.

Instead, use the jdk’s ClassValue to associate a jvm type to a value.

Is the ClassTag for an opaque type identical to its underlying type’s ClassTag? If it is supposed to be purely compiler side fiction, I would assume so. But in some sense ClassTag is ‘compiler side’ reflection, where the difference is visible.

5 posts were split to a new topic: On using Classes in Maps versus java.lang.ClassValue

I was looking at this in the Dotty 0.14.0-RC1 release notes. It’s very cool! I see some room for improvement there and this seemed like a good place to put my suggestion.

 implied arrayOps {
     inline def (arr: IArray[T]) apply[T] (n: Int): T = (arr: Array[T]).apply(n)
     inline def (arr: IArray[T]) length[T] : Int = (arr: Array[T]).length
 }

Having to repeat inline def (arr: IArray[T]) makes the above more cumbersome than AnyVal. I understand that IArray methods must be implemented as statics to avoid boxing, but defining them in the companion object doesn’t seem strictly necessary. There should be a way to add a bunch of methods at the same time. Also maybe take advantage of export ? I’m thinking that since an instance of IArray must be an Array, define IArray's methods in the type declaration, and use this for the instance.

opaque type IArray[T] = Array[T] {
  def apply(n: Int): T = this.apply(n)
  def length: Int = this.length
}

or

opaque type IArray[T] = Array[T] {
  export this.{apply, length}
}
3 Likes

I wish they had gone with inline class instead of the weird opaque type syntax.

inline class MyString(self: String) { def greet = s"hello $self" }

First, it would generally require less boilerplate, and second, it makes more sense conceptually with respect to companion objects: types cannot have companion objects, but classes can. The fact that opaque types are an exception and can have companion objects is fairly ugly.

For the first point, compare:

implicit inline class MyString(self: String) { def greet = s"hello $self" }

With:

opaque type MyString = String
object MyString {
  implicit def apply(self: String): MyString = self
  implied MyStringOps {
    def (self: MyString) greet = s"hello $self"
  }
}
8 Likes

I’m pretty worried about opaque types being extremely surprising in usecases where runtime types are inspected. Here are a view surprising cases…

object Foo { 
  opaque type Foo = List[Int]
  def from(list: List[Int]): Foo = list
}

val list = List(1,2,3)
val foo: Foo = Foo.from(list)

(foo: Any) match {
  case List(1,2,3) =>

  case _ => 
}

As a user I didn’t want to care about the runtime type of Foo. Now all of a sudden pattern matches may contain more edge cases and require a lot more thinking about, in order to defend against situations like these.

An other case, Map[Any, T] would squash these two values to be the same key.

Map(list -> 1, foo -> 2) // Map(List(1,2,3) -> 2)

Same with Sets:

Set(list, foo) // Set(List(1,2,3))

I know that there are some cases where we want to have both type-safety and performance, but I think that removing the ability to tell at runtime the difference between an opaque type and its underlying type will likely lead to quite a lot of surprises, and may end up as one of those things where we are telling noobies “oh yea, don’t use opaque types unless you know what you’re doing”.

Actually that was stated in the original post:

Thank you, yes I read that, and just wanted to say that I think that that behavior will be very surprising sometimes.

Hrm, you are not wrong but:

  1. You are effectively describing the difference between value classes (which aren’t going away) and opaque type aliases: value classes box in order to preserve identity whenever they are upcast (including when they are passed through generic parameters), whereas opaque type aliases don’t.
  2. Non-opaque type aliases, which are already in Scala, also don’t have an identity. The only difference is that, for them, the lack of separation exists even absent the upcast.
  3. More broadly, downcasts with pattern matching often carry this risk already: there is often a possibility that the object you are downcasting secretly mixes in one or more classes/traits that you are not aware of (usually because it was upcast before you ever saw it). In my personal opinion, the culprit here is the pattern of upcasting to Any and then performing an unsafe downcast after. I know that this pattern is really common and that my opinion isn’t going to stop people from doing it (and getting bitten by the results) but… I think that the incremental cost of introducing one more way to create unexpected results out of unsafe downcasts really isn’t going to change anything. Yes a few users will do things like what you are showing and get bitten… but those same users are probably already used to occasional strangeness out of their pattern matches.
1 Like

This might be a bit preposterous but has the idea of making regular type aliases opaque (i.e. types specified without the opaque modifier) been considered? If current style transparent type aliases still serve a useful purpose a transparent modifier could be added to them instead of adding the opaque modifier (the idea being that only a minority of type aliases would need to be defined as transparent; the rest would be better served by being opaque by default).

4 Likes

Would it be possible to reuse the keyword new instead of introducing opaque?

object Person {
  new type Id = String
}
2 Likes

My hunch is that the vast majority of type aliases out there serve mainly as shorthands and to re-export types defined in other scopes, and absolutely need to be transparent. I think defining an opaque type is always a deliberate decision, which has to be accompanied by the appropriate API definitions to enable actually interacting with the type.

1 Like

I agree with @LPTK’s hunch. And without actual evidence of the contrary, we should lean on the side that preserves the existing semantics of the code. Swapping the default would require strong evidence that opaque type aliases would far exceed transparent ones. That could be provided by a motivated person with an analysis of a large corpus of existing Scala code, comparing uses of type aliases versus usages of AnyVal classes that are not also implicit classes (the latter are going to become extension methods).

2 Likes

Would it be possible to reuse the keyword new instead of introducing opaque ?

Technically this would be feasible. It would be quite late to make that change, however, as opaque types have been accepted by the SIP committee. We can give it a quick discussion at the next meeting.

4 Likes

FWIW, when doing things like rendering graphics and generating SVG and such, “opaque” is a handy variable name. So personally I would prefer new.

You can use opaque as an identifier name in Dotty, it doesn’t clash with the use of opaque as a keyword.

Personally, I would function just fine with new type but I think it’s actually a bad name that we all happen to understand because of exposure to Haskell. I suspect that folks outside our bubble would appreciate opaque type much more.

3 Likes