Existentials and View

This problem arises in a SwiftUI thing, but my question is about how existentials are supposed to work.

Given this code

struct DiceBox: View {
    var lastRoll: Dice_Roll? = nil
    var body: some View {
        lastRoll != nil ? RollView(roll: lastRoll!) : EmptyView()
    }
}

The compiler complains that EmptyView and RollView are mismatched. What I want is for the system to not care, as long as it's a View. Changing the some to any brings up the error that the struct doesn't conform to View.

If I change the ternary to an if, this works, of course, but also takes up far more space.

What's the best course here?

This happens because any View doesn't implement the View protocol.

That's because an if statement inside the body closure goes through a ViewBuilder to build a View combining both branches.


struct DiceBox: View {
    var lastRoll: Dice_Roll? = nil
    var body: some View {
        if let lastRoll {
            RollView(roll: lastRoll)
        } else {
            EmptyView()
        }
    }
}

The type of body is actually _ConditionalContent<RollView, EmptyView> and that's the correct approach.

5 Likes

Note that you don't have to specify EmptyView explicitly.

Yes, it's three lines instead of just one, but if you value vertical space that much you can put it on a single line:

  // lastRoll != nil ? RollView(roll: lastRoll!) : EmptyView()
     if let lastRoll { RollView(roll: lastRoll) }

In this form it uses the same space vertically and even shorter space horizontally.

Perhaps the ternary operation could also be supported by the view builder machinery one day.

4 Likes

You can use Optional.map as well, for maximum terseness:

struct DiceBox: View {
    var lastRoll: Dice_Roll? = nil
    var body: some View {
        lastRoll.map { RollView(roll: $0) }
    }
}
4 Likes
lastRoll.map(RollView.init)
4 Likes

"This happens because any View doesn't implement the View protocol."

Yes, but why not?

See, in my mind, a protocol specifies an interface, and so it ought to simple be that if the struct has the members expected of a View, the fact that it has other members as well ought not matter.

I feel like it's a weird limitation. Imagine if you made a reservation at a restaurant, and they asked what style of car you would be driving. They don't care what it is, be it sedan, hatchback, pickup, they just want to know. And they need to know before you start driving. But as far as you can tell, the parking spots are all the same, all accessible, all the time.

That what it feels like to me.

This is prolly the right thing for this case. Thanks!

Consider a protocol P that requires a static property foo without a default implementation, meaning that users can get the value of this property for any conforming type without having an instance of that type. The existential type any P can't automatically pull an implementation of this static property out of thin air, so naturally it doesn't automatically conform to P .

In the future, Swift could consider allowing users to manually declare conformance of any P to P in an extension that manually supplies implementations of any such requirements. There hasn't been anyone working on this direction to my knowledge and it isn't possible to extend an existential type at all.

Even if that were to change, there will always be protocols with semantic requirements that are impossible even manually to conform. For instance, in the example above, if P is Animal and the static requirement foo is species , it is evident that any Animal cannot implement such a requirement: an existential box for an animal without an underlying animal type has no species. This shows that the existential type any Animal simply does not conform to Animal .

This is not a contrived example: substitute FixedWidthInteger and the static property bitWidth and we have an actual scenario from the standard library:

Any conforming type obviously must be able to implement the static property bitWidth without reference to any instance—that's fundamental to the semantics of the protocol. But any FixedWidthInteger is a box that can hold any fixed-width integer value: the same instance of this existential type can hold a UInt8 value today and a UInt16 value tomorrow. This demonstrates that any FixedWidthInteger is not a fixed-width integer type and therefore cannot conform to FixedWidthInteger.

9 Likes

Any conforming type obviously must be able to implement the static property bitWidth without reference to any instance—that's fundamental to the semantics of the protocol. But any FixedWidthInteger is a box that can hold any fixed-width integer value: the same instance of this existential type can hold a UInt8 value today and a UInt16 value tomorrow. This demonstrates that any FixedWidthInteger is not a fixed-width integer type and therefore cannot conform to FixedWidthInteger.

I find this odd, because I don't read 'any FixedWidthInteger' as a type, I read it a constraint.

Xiaodi Wu has that copypasta ready to go because people encounter your problem 523 times per minute, across Earth. (That's just the related internet post count from non-AIs; actual usage is estimated to be 700 times higher.)

associatedtype is literal. It's not "associatedtypeorprotocol". That necessitates workarounds like result builders, type erasure, and implicitly opened existentials.

This is not easy for humans to deal with; it's a workable model, but not good enough.

2 Likes

Keep in mind that there’s a distinction between P (type constraint), some P (specific but unnamed type that obeys that constraint), and any P (existential type). I have a feeling the confusion isn’t helped by the fact that Swift allows the last one to be spelled P in some situations.

1 Like

Yeah. I was hoping those things meant:

func f() -> some P {} //You are getting a concrete type conforming to P

func f(p: any P) {} //p can be any type, as long it conforms to P.

struct S: P {} //I pledge to conform to P

@bbrk24, thank your for the concise summary.

@Jessy, thank you for the statistics.

Could someone write a go-to article on this topic please?

2 Likes

That's exactly what those things mean. The key insight is that your definition of some P cannot hold an any P, because any P does not conform to P, for the reasons @xwu outlined above.

The other thing that doesn't help is that even though your code looks like it returns different types on each branch, the property in fact always returns a single type due to the result-builder transform.

There's no need to get existentials involved in this. I think that's a bit of a distraction - you were only drawn to try them because of the lack of support for ternary expressions in result builders. I can't imagine why ternaries would be singled out for exclusion (SE-0289 doesn't mention them at all), so this is probably an oversight and worth filing a bug report about.

Your issues here emphasise why they should be supported. You encountered an error and I think your attempt to solve it was logical and what I would expect most Swift developers to try, given our advice about what some and any types are for. But this is really all about a deficiency in the result builder transform, so the errors sent you down entirely the wrong path.

1 Like

You seem to be using "some P" and "any P" like concrete types. I see them as constraints. I read "some P" as "I'm going to hand you an instance which has the P interface", and "any P" is "I don't care what the type is, as long as it has the P interface."

In no way do I think there is some intermediate representation which you can interact with. Maybe I'm wrong? I regard them as something which gets taken care of in the compilation.

In fact, what I thought it was doing was letting me write more concise code. Let me give you my first example:

This is code I current use:

import SwiftUI

struct HVStack: View {
    var vertical = true
    @ViewBuilder var view: any View
    var body: some View {
        if vertical {
            VStack {
                AnyView(view)
            }
        } else {
            HStack {
               AnyView(view)
            }
        }
    }
}


struct HVStack_Previews: PreviewProvider {
    static var previews: some View {
        HVStack(vertical: true) {
            Text("Hi There")
            Text("Jelly")
        }
    }
}

What I was hoping was that the "any View" meant I didn't have to wrap view in AnyView. It seems redundant.

(Even better would be if I could say let Stack = vertical ? VStack : HStack; return Stack(view). One day?)

1 Like

Could someone write a go-to article on this topic please?

I'd sure love one.

To the compiler, they are types. We refer to any P as an “existential” type, but that’s just terminology; it’s still a type just as much as HStack is.

I’ve written my own extension method on View to facilitate this:

var body: some View {
  view
    .apply {
      if vertical {
        VStack($0)
      } else {
        HStack($0)
      }
    }
}

Still more verbose than if the ternary were supported in that way, but far better than repeating the contents in many cases.

The implementation of `apply`
extension View {
  func apply<V: View>(@ViewBuilder transform: (Self) -> V) -> V {
    transform(self)
  }
}
3 Likes

You're gonna love the modern solution. :melting_face:

1 Like

Also, I actually have no idea why

let view: any View = EmptyView()
AnyView(view)

compiles but

AnyView(EmptyView() as any View)

does not. Anyone know that one?

1 Like