SE-0335: Introduce existential `any`

Would You mind telling me the relationship between SE-0335 and Generalized existential?
If we have the support with Generalized existential, do we need SE-0335 ?
Thanks a lot.

Is this still relevant? Reading the source code, that's not what I see. Am I looking in the wrong place?

Another recent example:

1 Like

This proposal is orthogonal to generalized existentials. This proposal adds an explicit keyword for writing existential types, but it does not allow you to express additional constraints on existential types. Similarly, generalized existentials would not give us an explicit, searchable keyword for writing existential types.

The technique remains relevant, even if the compatibility window for needing this for the specific marker protocol Sendable has passed.

Doug

Moved to a separate thread - Introduce existential `any` - avoiding inconsistency around `Any` and `AnyObject`

As John mentioned above, this is a proposal review; can we split this off to a separate discussion thread?

If I read this correctly, does that mean we can only declare typealias Foo = Any, and that declare Foo as both constraint and existential type? Particularly, can I do this?

typealias Foo = Any

protocol P1: Foo { ... }
func bar(foo: Foo)

I think the more important part about typealias is that it now separates existential aliasing from constraint aliasing. Breaking that property would make refactoring non-trivial. If for example we original used typealias Foo = any and use them as both constraint and existential, such as the code above, then changing Foo to anything else, e.g., Foo = Codable or Foo = any Codable would break the codebase.

That why I think the important part is that we can immediately tell from typealias whether it is aliasing types, constraints, or existential. Maybe we can instead disallow any Any only outside of typealias?

On that note, I think the Any and AnyObject interaction with typealias could be better clarified in the proposal.


For motivation, I think we should focus more on the capability differences between generic and existential. I don't think people are bitten as much about performance (nor are they particularly concerned). It's more important that generic can

  • use static members,
  • conform to the itself, or anything really,
  • access associated type, and
  • convert to existential,

while existential can only support different member types at run time* at the cost of everything above. So most likely one would use generic when they mean to use unspecified type conforming to certain constraints. The proposal did include the associatetype portion, so we can expand a bit more on that.

* Theoretically, we can add openExistential to convert existential to generic types, but then you're still more likely to prefer to use generic type directly for performance reasons.


Overall, I doubt a mere keyword would make it obvious all the nuance between different choices (existential vs generic vs opaque), but I find the separation between constraint and existential invaluable. Especially since existential behaves much more like concrete types and generics than protocol constraints.

I think the proposal should clarify that, like their base types, Any.Type and AnyObject.Type will also get special behavior to be treated as existential.

3 Likes

I'm with you on this.

-1

It feels so much like a step backward in the language (and boy it sure looks an awful lot like id<SOMEProtocol> from the yesteryears of Objective-C). I don't see the benefit other than "warning them that what they're doing is expensive and sub-optimal".

... but the language makes existential types too easy to reach for, especially by mistake...

Ok? So are we going to hand-hold programmers with all memory issues now? Seems like not a good place for the language to take over the architecture. Also "too easy"? Isn't that the point of how it was written in the first place?

Sorry to be pinging this, but could someone clarify this part?

Thanks for the ping!

I thought about the use case for typealiasing Any a bit more and I think this example should be the same as other type aliases that can be used as generic constraints:

typealias Foo = Any

protocol P1: Foo { ... }
func bar(foo: any Foo)

Otherwise, when the type alias switches from Any to e.g. Sendable, the code would break (under a language mode where any is required). The implementation already knows how to infer any specifically for associated type inference, and we could keep that behavior around for Any and AnyObject under the language mode where any becomes more strict, but this would leave a couple weird inconsistencies:

protocol P {
  associatedtype A
}

struct S: P {
  // this is a type witness, so it has to be existential
  typealias A = Any

  func generic<T: P>() -> T.A: A { ... } // error, `A` is really `any Any` which isn't a valid conformance requirement
}

This inconsistency is only an issue if the type witness is explicitly written with a typealias. We can fix this by always requiring any for explicit type witnesses, even for Any and AnyObject, like you suggest. This also means getting rid of the any Any warning, which I'm very indifferent about anyway, and it seems like some folks in this thread would prefer to write any Any for consistency. And FWIW, this example is super contrived and I don't imagine that using Any or AnyObject as a type witness is particularly common.

In the vast majority of cases where a typealias is used as both an existential and a conformance constraint, the refactoring amounts to adding any in front of the type alias name where used as an existential. It's only in the case where a type alias is used for both and it satisfies an associated type requirement that the type alias must be split into two different ones (once any is required) because a protocol is not a valid type witness. This case seems pretty rare, and the transformation still seems straightforward enough to be performed by a migrator.

I agree. I'll clarify this behavior in the proposal.

2 Likes

Thanks for clarification, it's much clearer now.

In this case, since we're treating typealias Foo = Any as protocol, we lose the ability to directly make an alias to its existential typealias Bar = any Any. The workaround is simple, though; we can just do

typealias Foo = Any
typealias Bar = any Foo

so it's not particularly a problem whether we reject direct Any existential in type alias or specifically allow any Any in there.

However, since we might be allowing/requiring any Any in associated types. It might be better to make the rule instead that, any is required for all (existential) type aliases, even for Any. That might be an easier rule to work with.


Agreed. This should be a pretty rare occurrence. I don't have a strong opinion either.

I agreed that it seems automatically migratable.

I was thinking about the special case for Any after any is required, which I assumed that its type alias would remain as both existential and protocol. Since that's not the case, please kindly ignore it.

1 Like

I would urge you to maintain the rule that a typealias to Any can continue to serve as both protocol constraint and existential type without warning. As @Lantua points out, otherwise a user cannot typealias existential type Any without jumping through hoops since you also propose to disallow the spelling any Any.

That definitely suffices as an alternative resolution to the problem though :)

2 Likes

And let's make sure, at the same time, that we support the technique described here, by not emitting any warning for any Foo even if Foo is an alias to Any:

#if swift(>=5.5) && canImport(_Concurrency)
public typealias Foo = Swift.Sendable
#else 
public typealias Foo = Any
#endif

By the way, when discussing changes to Any, there is one secret/magical use-case that I hope is maintained:

typealias IsRandomAccessCollectionOfIntegers<T> = T where T: RandomAccessCollection, T.Element == Int

func doSomething<Source>(
  _ source: Source
) where IsRandomAccessCollectionOfIntegers<Source>: Any {
//                                                ^^^^^ - this is where the magic happens :)
  let _: Int? = source.first  // βœ…
}

func useDoSomething() {
  doSomething([1, 2, 3])  // βœ…
  doSomething(["one", "two", "three"])  // ❌ Cannot convert value of type 'String' to expected element type 'Int'
}

Basically, it allows you to write "constraint aliases" and simplify repeated, verbose generic constraints.

I suspect this is an implementation accident, and I wouldn't be surprised if it isn't tested either in the compiler's test suite or the source-compact suite, but it's awesome and I hope it doesn't get broken unless it is being replaced by true constraint aliases (which would be a good thing to add as part of the overall work to make generics more approachable, IMO).

11 Likes

This is completely inscrutable and super neat!

7 Likes

I agree:

This feature is called requirement inference. You could achieve the same thing using a same-type constraint IsRandomAccessCollectionOfIntegers<Source> == Source if you only want to write the type alias in the where clause (except the compiler currently warns that this same-type constraint is redundant even though it adds requirements via inference). In any case, I'm not proposing to remove the ability to write an unconstrained constraint. There would also be no replacement for some Any, which could be useful for some of the future directions around using opaque types in parameter position.

7 Likes

Hey all,

Thanks for the fantastic discussion. The Core Team has discussed this proposal and accepted it with modifications. The review result is here.

Doug

12 Likes

Which compiler flag will enable this warning in Swift 5.6?