Opaque type alias

Disclaimer: I am by no means a compiler/language expert, though I think this discussion could be useful/interesting.

In so far, this

some View

tells us a few things:

  • It is a concrete type conforming to View,
  • We don't know/care what the actual concrete type is, and
  • It appears only once, all other declarations may use different concrete types.

The last one hinders the ability to express relationships between types. For example, I can not express that two functions return the same opaque types. Given this example,

protocol RangeProtocol {
  associatedtype Index: Comparable
  var startIndex: Index
  var endIndex: Index
}

struct Foo: RangeProtocol {
  var startIndex: some Comparable { 0 }
  var endIndex: some Comparable { 0 }
}

It doesn't work today because the compiler doesn't know the that the type of startIndex and endIndex are the same. Even if the compiler does, it does so by examining the concrete type of each property, or the fact that Foo conforms to RangeProtocol. Nothing in the declaration of startIndex and endIndex express that they should be of the same type, and at the same time opaque. This also applies to protocols with a lot of intertwined associated types like Collection.

Similar thing applies to global functions,

func foo() -> some FixedWidthType { ... }
                      |
func bar(_: some FixedWidthType) { ... }

There's no way to express that the return type of foo is the same as the argument of bar.

So, how can I have my cake and eat it too. I think we should add opaque type alias. Say, if I want to have an opaque type named OpaqueFoo that conforms to FixedWidthInteger and is Int underneath, I could write

public internal(opaque) typealias OpaqueFoo = opaque Int where OpaqueFoo: FixedWidthInteger

It is still opaque in a sense that, outside the opaque scope (internal), one can only treat OpaqueFoo as FixedWidthInteger. This does allow us to express the type relationship between variables while maintaining the opaqueness.

public func foo() -> OpaqueFoo { ... }
public func bar(_: OpaqueFoo) { ... }

public struct SomeCollection: Collection {
  public private(opaque) typealias Index = opaque Int where Index: Comparable
  public var startIndex: Index { ... }
  ...
}

Looking back at the some Protocol notation, it could be treated as a short hand for one-time-use opaque type alias

/// This
func foo() -> some Comparable { 4 as Int }

/// is short hand for this
typealias _Unnamed = opaque Int where _Unnamed: Comparable
func foo() -> _Unnamed

This notion also runs in parallel with generic should we add things like some Protocol that came up in Improving the UI of generics post as a one-time generic

/// This
func foo(_: some Comparable) { ... }

/// is short hand for this
func foo<T: Comparable>(_: T) { ... }
1 Like

I think there’s been a misunderstanding. any Comparable is intended to be a revised notation for the existential type, to clarify the distinction between the type and the protocol itself. The pitched shorthand for a generic parameter is some Comparable. That is:

func f(_: some Comparable) { ... }
// would be shorthand for:
func f<T: Comparable>(_: T) { ... }

func f(_: any Comparable) { ... }
// would be modern notation for:
func f(_: Comparable) { ... }
// which will only be possible if we relax rules around PATs

An existential specializer pass can optimize the most obvious concrete uses of the latter to the former, but they are not the same thing!

3 Likes

It's been a while, I should have checked. Anyhow, fixed.

I've been intending to write a pitch for opaque type aliases myself for a while, for scratching a pet peeve when dealing with data from REST APIs:
Often, you get an object with an ID in the shape of a number or a string, and the common case is to store it as an Int or String, perhaps adding Identifiable conformance to the type.

However, since all Identifiable gives you is a typealias ID = Int, you can still compare the ID of a customer and the ID of a supplier, or even do arithmetic or string manipulation on said IDs since they are just transparent type aliases.

Being able to specify a

struct Customer: Identifiable {
    typealias ID: some Comparable
    let id: ID { return 4 }
}

would help separate the implementation details of an ID type from its specific type (subject to change as your APIs develop) and make the ID only support relevant methods for IDs, such as comparison.

1 Like