Implicitly Opened Existentials on Initialization

After landing SE352

We can use the following code

protocol P {
  associatedtype A
  func getA() -> A
}

func openSimple<T: P>(_ value: T) { }

func testOpenSimple(p: any P) {
  openSimple(p) // okay, opens 'p' and binds 'T' to its underlying type
}

I was wondering can we move further to open existential for something like this?
(Currently the following code will give an error)

protocol P {
  associatedtype A
  func getA() -> A
}

struct S<T: P> {
    var s: T
}

func testOpenSimple(p: any P) {
  let _ = S(s: p) // **error**, Type 'any P' cannot conform to 'P'
}
1 Like

There is a temp workaround for solving this - Add a function to bridge.

But still we can't get a concrete type S variable from any P(which means opening existential on any P)

protocol P {
    associatedtype A
    func getA() -> A
}

func openSimple<T: P>(_ value: T) {}

struct S<T: P> {
    var s: T
}

func testOpenSimple(p: any P) {
    openSimple(p) // okay, opens 'p' and binds 'T' to its underlying type
    // _ = S(s: p) // **error**, Type 'any P' cannot conform to 'P'

    func bridge1<T: P>(_ s: T) {
        _ = S(s: s)
    }
    bridge1(p) // okay, we use a function to bridge the init usage

    func bridge2<T: P>(_ s: T) -> S<T>{
        S(s: s)
    }
    // _ = bridge2(p) // **error**, Type 'any P' cannot conform to 'P'
}

would changing any to some help?

func testOpenSimple(p: some P)

"some P" is not a existential type.

The premise of the problem is how to open the existence type in some condition.

A specific example is the following :point_down::

You can clone it locally and build it with Swift 5.7 to see the build errors.

struct ContentView: View {
    var keypadsLayout: [[any Keypad]] {
        [[.zero, .dot, .equal]]
    }

    var body: some View {
        ForEach(keypadsLayout, id: \.description) { keypads in
            ForEach(keypads, id: \.title) { keypad in
                KeypadView(calculator: calculator, keypad: keypad)
            }
        }
    }
}

struct KeypadView<KeypadType: Keypad>: View {
    var keypad: KeypadType
    var body: some View {...}
}

In this kind of initializer, you're implicitly using the generic type twice — once in the parameter and again in the return type. SE-352 allows the implicit opening of existentials only when the generic type occurs exactly once in the function signature.

It seems possible that the compiler could be made to special-case this scenario and substitute the opened type in both places, but maybe there's a technical reason why not.

For now, though, the compiler is correctly enforcing the SE-352 rules.

When you invoke an initializer, it's acting like a generic function call of type <T:P> (s: T) -> S<T>. If we open p, that calls the initializer by binding T to the dynamic type inside the value of p, so the call becomes (s: {dynamic type of p}) -> S<{dynamic type of p}>. SE-0352 backed off from providing a way to refer to {dynamic type of p} after p is opened, and instead chose to leave cases like this where the dynamic type shows up as part the return type unsupported. The diagnostic should be improved to make this more obvious.

If it makes sense to make the generic S type conform to a protocol, then instead of calling the S initializer directly, you could use a static factory method that returns the S as a protocol existential or opaque return type, which you can then use with an opened existential:

protocol Q {}
struct S<T: P>: Q {
  var s: T

  static func make(s: T) -> some Q { return S(s: s) }
}

func testOpenSimple(p: any P) {
  let q = S.make(s: p) // q has type `any Q`
}
3 Likes

This doesn't seem to compile for me with Xcode 14b2

Thanks, that looks like a bug to me.

Okay, cool. We are hitting this exact same issue, it's nice to know that using a static factory method should be able to get around it for now.

Joe, should I log a bug in the Swift repo? Would you be able to help me articulate the problem in the issue?

Filing an issue on our Github would be good, yeah. If you can post an example of the code you expect to compile, or even the example I just provided, that should be sufficient. Thank you!

Will do, thanks!

issue submitted: Failure to implicitly open existential on initialization through factory method · Issue #59851 · apple/swift · GitHub

1 Like

We can workaround again by using bridge method lol

func testOpenSimple(p: any P) {
    // Error
    //  let q = S.make(s: p) // q has type `any Q` 

    func bridge2<T: P>(_ s: T) -> some Q {
        S.make(s: s)
    }
    
    _ = bridge2(p)
}

Even with this workaround, my specific example on calculator still can't compile.

Maybe it is relavent with "some View"?

struct ContentView: View {
    var keypadsLayout: [[any Keypad]] {
        [[.zero, .dot, .equal]]
    }

    var body: some View {
        ForEach(keypadsLayout, id: \.description) { keypads in
            ForEach(keypads, id: \.title) { keypad in
                // KeypadView.workaroundInit(keypad: keypad) // Error
                // bridge(keypad: keypad) // Error
                // bridge2(keypad: keypad) // Error
            }
        }
    }
    private func bridge<K: Keypad>(keypad: K) -> some View {
        KeypadView.workaroundInit(keypad: keypad)
    }

    private func brdige2(keypad: any Keypad) -> some View {
        bridge(keypad: keypad) // Error Type 'any View' cannot conform to 'View'
    }

    private func brdige3(keypad: any Keypad) -> any View {
        bridge(keypad: keypad) // Success
    }

    private func brdige4(keypad: any Keypad) {
        _ = bridge(keypad: keypad) // Success
    }
}

struct KeypadView<KeypadType: Keypad>: View {
    static func workaroundInit(keypad: KeypadType) -> some View {
        KeypadView(keypad: keypad)
    }

    var keypad: KeypadType
    var body: some View {...}
}

@Joe_Groff Could you help look at this and provide some workaround suggestion? Or should I submit another Github issue to track this?

In that case, I think some kind of error is appropriate, although the generic error you're getting is not very descriptive. Calling bridge(keypad: keypad) inside of bridge2 passes the dynamic type into bridge, but then re-boxes the return value of bridge into an any View. Since bridge2 is declared to return a concrete value of some View type, any View doesn't satisfy that return type requirement. It's still worth filing a bug to improve the diagnostic.

Is this bug the reason why we cannot use @ObservedObject in generic views constrained to protocols yet?

E.g.

import PlaygroundSupport
import Foundation
import SwiftUI

protocol P: ObservableObject {
    var hello: String { get }
}

final class Implementation: P {
    @Published var hello: String = "Hello"
}


struct Sample: View {
    let something: any P = Implementation()
    
    var body: some View {
        VStack {
            PView.make(something) // Type 'any P' cannot conform to 'P'
            createPView(something) //  Type 'any P' cannot conform to 'P'
        }
    }
    
    func createPView<T: P>(_ s: T) -> PView<T> {
        PView(s)
    }
}

struct PView<T: P>: View {
    @ObservedObject var p: T
    
    public init(_ p: T) {
        self.p = p
    }
    
    var body: some View {
        Text(p.hello)
    }
    
    static func make(_ s: T) -> some View { PView(s) }
}

PlaygroundPage.current.setLiveView(Sample())

Or am I doing something else wrong here?

You can't have it both ways. While you can use a generic type inside the function by opening the existential in some way, you can't return it or use it in the main body of the function, since it requires the generic parameter.

func makeArray(withOneValue: Any) -> Array</* what would you write here? */> {
  let array: Array</* what would you write here? *?> = [withOneValue]
{