Possible compiler bug with opaque types? Or is this intended behavior?

My understanding of opaque types is that if a function returns an opaque type, the compiler knows what type is being returned even though it's not explicitly defined in the return type itself.

Since it does give us the guarantee that it knows what the type is, then why does it pretend like it doesn't know what the type is?

In the below example, this Combine publisher clearly publishes a value of type Int, and the compiler knows it, because if I try to make a condition where sometimes it publishes a String then the compiler won't allow it.

However despite the fact that the compiler knows this publisher's value is Int, when we try to treat it as an Int in our subscriber, it doesn't let us do that because it says the type is simply '(some Publisher).Output'.

The only alternative seems to be, use the (partially) type-erased AnyPublisher as a return type, and make the generic constraints explicit like AnyPublisher<Int, Never>. But then we have to resort to arcane stuff like appending .eraseToAnyPublisher which does not feel like nice, clean, Swifty code.

So is this a compiler bug or is some Publisher supposed to hide the exact type from the subscriber?

If it is supposed to hide it, then could I humbly request that we also add a feature to the Swift language called "transparent types", which acts just like "opaque types" except without the loss of type information?

Maybe the syntax for "transparent types" could be somePublic?

Or is there a reason why information loss had to be coupled to this new ability of the compiler to infer the precise return type?

import Foundation
import Combine

/// Sends a single value and then completes.
func sendsSingleValueThenCompletes() -> some Publisher {
    Just(1)
}

sendsSingleValueThenCompletes()
    .sink(
        receiveCompletion: {
            print("Completion: \($0)")
    },
        receiveValue: {
            print("Value: \($0.bitWidth)")
            
            // error: Combine Lab - Solutions.xcplaygroundpage:16:32: error: value of type '(some Publisher).Output' has no member 'bitWidth'

    }
)

The underlying type for some Publisher here is Just<Int>, not Int.

That is precisely why the feature exists, and it's exactly why it's referred to as 'opaque.'

A 'transparent' type would be the concrete type itself, and there is no loss of type information with an opaque type.

func opaque<T: Numeric>(_ x: T) -> some Numeric { x }
print((opaque(42) as! Int).bitWidth)

I don't believe this is quite what @1oo7 is getting at—it's entirely reasonable to want to hide the concrete type that witnesses a protocol while retaining the type information that is part of the protocol itself. I think what @1oo7 is reaching for here is the (not-yet possible) some Publisher P where P.Output == Int extension of opaque types.

But, @1oo7, as things stand today, @xwu is correct. some Publisher expresses exactly what it sounds like in natural language—as an API consumer you have some concrete type that witnesses the Publisher protocol, but you don't know anything else about that type (in particular, you don't know anything about its associated type requirements).

2 Likes
  1. The compiler knows the associated types (assuming things are in the same module). It's not trying to pretend that it doesn't know anything. (That said, we should improve the diagnostic. EDIT: I've filed [SR-12896] Improve diagnostic for opaque types + property/method access · Issue #55342 · apple/swift · GitHub)
  2. What you are asking for, if I'm understanding correctly, is a way to expose limited additional information about an opaque type without giving away the underlying concrete type. There are several things here:
    • IMO, the correct terminology would be translucent types, not transparent types. I believe using the term "transparent" is more likely to lead to confusion; that makes it seem like you want to expose all the information; if that's the case, why not use the concrete type instead of using an opaque type?

    • Based on my (cursory) reading of the opaque types proposal, this is not listed as a future direction in the proposal.

    • You can implement this in a one-off way by creating specialized protocols, although it's not a general solution, and not ergonomic.

      // Haven't tested, should work probably?
      protocol IntPublisher : Publisher where Self.Output == Int {}
      extension MyPublisher : IntPublisher {}
      

@typesanitizer I think this is basically covered by the "fully generalized reverse generics" link at the bottom of the proposal, which links to this section of the Improving the UI of Generics thread. The syntax is slightly different from some Protocol, but it fills the same role:

func evenValues<C: Collection>(in collection: C) -> <Output: Collection> Output
  where C.Element == Int, Output.Element == Int
{
  return collection.lazy.filter { $0 % 2 == 0 }
}
2 Likes