Is this still relevant? Reading the source code, that's not what I see. Am I looking in the wrong place?
Another recent example:
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.
Sorry to be pinging this, but could someone clarify this part?
Particularly, can I do this?
typealias Foo = Any protocol P1: Foo { ... } func bar(foo: Foo)
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.
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.
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.
On that note, I think the
Any
andAnyObject
interaction withtypealias
could be better clarified in the proposal.
I agree. I'll clarify this behavior in the proposal.
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.
I don't imagine that using
Any
orAnyObject
as a type witness is particularly common.
Agreed. This should be a pretty rare occurrence. I don't have a strong opinion either.
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.
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.
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 whereany
is required).
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
.
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 writeany Any
for consistency.
That definitely suffices as an alternative resolution to the problem though :)
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.
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).
This is completely inscrutable and super neat!
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 ifFoo
is an alias toAny
I agree:
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 whereany
is required).
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
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.
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
Which compiler flag will enable this warning in Swift 5.6?
Which compiler flag will enable this warning in Swift 5.6?
There are none (by design):
There isn't currently a way to turn on warnings for missing anys in your code. That's intentional; we're working on a lot of generics ergonomics features right now that may change the way programmers update their current use of existential types. A lot of Swift code today uses existential types where it doesn't need to because existential types were the more natural thing to write. With opaque parameters and existential opening for function arguments that's being worked on right now, a lot of cβ¦