Out of Line Initialization of Opaque Types

As I understand it, opaque types must be initialized inline. This compiles:

let publisher: some Publisher<Int, Never> = Empty()

But this does not:

let publisher: some Publisher<Int, Never>

publisher = Empty()

Has enabling such out-of-line initialization of opaque types been considered as a future direction? Does this pose any challenges for the compiler?

The desire for this came up when I wanted to refactor a protocol that declared a type-erased member (declared as any Publisher...) to declare the member type as an associatedtype. A few of the implementations of this protocol construct this member through a rather elaborate chain of publisher operators, and also needs to store it instead of computing it (to share it). I realized that I can't declare the member as some Publisher... but then initialize it in the type's init.

That might require solving more problems, i.e. deriving an associatedtype from an out-of-line initialized opaque type. But I couldn't find any other way to solve my problem without regressing those particular implementations back to AnyPublisher, or trying to painstakingly spell out the complicated publisher type in the member declaration.

1 Like

If I understood your problem correctly, here is a workaround for you:

protocol Protokol {
  associatedtype Pub: Publisher<Int, Never>
  var publisher: Pub { get }
}

protocol StaticFactory {
  associatedtype Input
  associatedtype Output
  static func make(_ input: Input) -> Output
}

struct Impl: Protokol {
  enum PublisherFactory: StaticFactory {
    static func make(_ input: Int) -> some Publisher<Int, Never> {
      Just(input) // or whatever
    }
  }

  let publisher: PublisherFactory.Output

  init(arg: Int) {
    publisher = PublisherFactory.make(arg)
  }
}

1 Like

Here's a simpler workaround:

struct MyTypeErasedPublisher {
  private var _publisher: Just<Int>
  var publisher: some Publisher<Int, Never> {
    _publisher
  }
}

That way you store the actual type, but only allow access to the opaque type.

In general I'd rather we go in something like this direction where it's possible for the declaration author to specify what the concrete underlying type will be without requiring it to be derived from the initial value expression. Of course, SE-0360 means there won't always be a single static type that you could do this with...

Awesome! This enabled to me to avoid spelling out the full type. Thank you!

This requires you to spell out the actual type. In this example it's Just<Int>, but the situation I'm in is more like this:


  let publisher: some Publisher<(T, T), Never> // What I wish I could do

  init(
    value1: Int, 
    source1: some Publisher<T, Never>, 
    value2: Int,
    source2: some Publisher<T, Never>
  ) {
    publisher = source1
      .prepend(Just(value1))
      .combineLatest(
        source2.prepend(Just(value2))
      ) { first, second in (first, second) }
      .share()
  }

I'm not too concerned about encapsulating the real type (although it should be, that's just a bonus), but rather having the compiler infer the real type. Not only can spelling it out be quite difficult, I'm not sure it's even possible to do so if somewhere in that sequence of operators an underlying opaque type is brought in.

And maybe that's the problem: I'm treating some ... as Swift's version of C++'s auto, which the initial proposal directly addressed, stating in no uncertain terms that is not what it is.

But either way it made me wonder if there's a significant challenge to deriving the real type from an out of line initialization. If the compiler can already prove that an out of line initialization (of both local and instance variables) is correctly typed (including that multiple runtime pathways all initialize to the same type), does it essentially already have this capability with opaque types?

2 Likes

IMO deduction of the type of a variable from anywhere other than the declaration site should be considered very carefully. The locality of the variable declaration and its type is important for reading. Losing the enforcement of this locality may affect the readability of the language.
Also it opens question should we allow this only for variables with opaque types or for all variables?

let foo: some // literally no constraint is applied
// ... 42 lines later
foo = someExpression()

An alternative approach is to introduce two pseudo properties on function types (like .self):

  • .ReturnType - a type
  • .ArgumentTypes - a parameter pack or a tuple

Expressions with these properties are treated as types:

func foo() -> Int { ... }

let bar: foo.ReturnType // resolves to `Int` at compile time
4 Likes