Hello Swift community,
I have a pitch for you about changing the default meaning for plain protocols. Please share any thoughts, questions or suggestions!
- Proposal: SE-NNNN
- Authors: Angela Laar, Holly Borla
- Review Manager: TBD
- Status: Awaiting implementation, implemented on
mainbehind the experimental flag-enable-experimental-feature ImplicitSome
Previous Swift evolution discussion: [Discussion] Eliding `some` in Swift 6
Introduction
Currently, the default for writing a plain protocol name in type position is any, but many of these implicit any uses could be replaced with some, giving them more type information while still behaving correctly. This proposal flips the default so that when writing plain protocols, the type would default to some instead of any. Making the default some guarantees a fixed underlying type which preserves static type relationships to the underlying type, giving you full access to the protocol requirements and extension methods that use Self and associated types.
Motivation
Swift has been working towards improving UI for generics for some time with the introduction of the some keyword in Swift 5.1 to represent opaque types ā abstract type placeholder for a specific concrete type ā which was extended to parameters in Swift 5.7 type such that:
func foo<T>(_ bar: T) where T: Equatable { }
// is equivalent to
func foo(_ bar: some T) { }
Swift 5.6 introduced explicit any, which will be required in the Swift 6 language mode, to ensure that opting-in to type erasure instead of using type parameters is explicit and deliberate. Introducing the requirement of using explicit any and encouraging writing some by default opens an opportunity to make some the default for plain protocol names.
Generic code is already simplified in more places than you think. Take protocol extensions, for example. To write generic code applicable to any concrete type conforming to a protocol, you only need to write the extension keyword and the plain protocol name. In this example we are working with the Collection protocol:
*// What you write to extend all concrete types conforming to 'Collection':*
extension Collection { ... }
In generic code, a generic signature represents generic parameters and any requirements on those parameters. In the case of protocol extensions, thereās an implicit type parameter called Self and single conformance requirement Self: Collection that are added by the compiler instead of needing to be written by you. This allows you to access all protocol requirements, associated types, and other extension methods from the Collection protocol on the conforming Self type.
It is much easier for programmers to learn generic programming when there are stepping stones that donāt require internalizing generic signatures with where clauses. Programmers can use more straightforward and intuitive syntax to express the same thing. Swift 6 could apply this same principle to plain protocol names in other contexts. Such a practice could be invaluable for beginners learning Swift, removing the mental load of comparing tradeoffs between some and any when adding protocols to their code. Beginners donāt need to decide between using some vs. any, postponing the need to fully understanding the semantic differences until it becomes absolutely necessary to choose between the two. Even if youāre not a beginner, some as the default will still improve readability of your code by making it more concise.
Why is some a better default?
any provides a common supertype for all concrete types that meet the given requirements. Using any is a form of type erasure that allows you to store any arbitrary subtype dynamically by using a boxed representation in memory. Type erasure has a number of use cases where you need the ability to change or mix different underlying types, such as:
-
Local
let-constants with different initial types across different control-flow paths andvars whose underlying type changes -
Heterogeneous collections, e.g.
[any DataSourceObserver] -
Optionals that are assigned an underlying type later, e.g.
var delegate: (any UITableViewDelegate)? = nil -
Stored properties of types that use a protocol abstraction, but the enclosing type is not parameterized. The delegate pattern above is a common example of abstraction as an implementation detail of a concrete type.
Protocol requirements and extension methods are not available for any types. If the underlying type can vary, so can the protocol requirements that depend on that type. So there is no way to know what the generic signature of a requirement using Self or associated types is, in a contravariant or invariant position. For this reason, programmers are encouraged to write some by default and change some to any when storage flexibility is needed. This workflow is similar to writing let constants by default until mutation is necessary.
Writing any explicitly is more important because programmers need to understand the type-erasing semantics and the limitations of using an existential type, including the ability to change the underlying type, the inability to access Self and associated type requirements, etc.
In many use cases for some, itās already evident in the code that the underlying type will never change. For example, a let constant or a non-inout parameter already indicates that the underlying type cannot change. Similarly, opaque result types in single-return methods and result builders clearly have one underlying return type. In these cases, writing some would be redundant.
Easing the migration to Swift 6
Swift 6 will require explicit existential syntax introduced in SE-0335. Bare protocols as types will no longer compile; they must be prefixed with either the any keyword or the some keyword:
protocol P {}
struct S {}
let y: P = S() // error
let x: any P = S() // okay
If, instead, we switch the plain protocol syntax to mean some instead of any, code churn will significantly decrease. This would mean in Swift 6 plain protocol name P could still be valid code in many cases, but it would mean some P instead of any P.
Consider the following function. Currently, the function parameter uses implicit any, where some would be a better fit. Since the underlying type is not expected to change, there is no need to opt-in to type erasing behavior.
public protocol BlogPost { ... }
public func controller(for post: /*implicit any*/ BlogPost) -> BlogPostDataController {
let key = CacheKey(id: post.id)
if let postController = cache[key] as? BlogPostDataController { return postController }
let postController = BlogPostDataController(post: post)
cache[key] = postController
return postController
}
// Example call-site
let controller = controller(for: Tutorial(...))
If the default for a plain protocol name changes to some, this function will behave the same way with more type information preserved at compile-time, and notably, the code will not require any changes when upgrading to Swift 6:
public func controller(for post: /*implicit some*/ BlogPost) -> BlogPostDataController {
let key = CacheKey(id: post.id)
if let postController = cache[key] as? BlogPostDataController { return postController }
let postController = BlogPostDataController(post: post)
cache[key] = postController
return postController
}
// Example call-site
let controller = controller(for: Tutorial(...))
Using implicit some, this codeās intention is still clear; a concrete type conforming to the BlogPost protocol is needed to query the cache and create instances of BlogPostDataController.
Proposed solution
Instead of always writing some or any in Swift 6, I propose we make generic code for protocols more concise and approachable by eliding the some keyword. With this approach, type annotations will be more lightweight and readable.
For example in Swift 5.8, you can write a zip function using the some keyword for both arguments and the result type:
func zip<E1, E2>(_: some Sequence<E1>, _: some Sequence<E2>) -> some Sequence<(E1, E2)>
Eliding the some keyword, this declaration would read:
func zip<E1, E2>(_: Sequence<E1>, _: Sequence<E2>) -> Sequence<(E1, E2)>
The above declaration is conceptually the same as using explicit type parameters for each appearance of Sequence:
func zip<S1, S2, E1, E2>(_: S1, _: S2) -> some Sequence<(E1, E2)>
where S1: Sequence, S2: Sequence, S1.Element == E1, S2.Element == E2
Detailed design
A plain protocol name P, or typealias thereof, resolves as some P in the following contexts (some of which will produce a compiler error per the restrictions on opaque types):
- Function parameter lists, e.g.
func f(_: P) - Function return types, e.g.
func f() -> P - Local variables, e.g.
let x: P = ... - Generic argument lists, e.g.
let x: Generic<P> = ... - Primary associated type argument lists for opaque types, e.g.
let x: some Collection<P> = ... - Same-type requirements, e.g.
T.Element == P
The same restrictions on opaque types apply to plain protocol names. There are some places where you cannot use an implicit type parameter, including:
- Primary associated type argument lists of existential types, e.g.
any Collection<some P> - Variable declarations without an initial value, e.g.
let value: some P - Generic requirements, e.g.
T.Element == some P
Plain protocol names are still interpreted as conformance requirements in the following contexts:
- Inheritance lists, e.g.
Pinstruct S: P {} - Generic conformance requirements, e.g.
Pinfunc test<T>() where T: P - The right hand side of a type-alias declaration, e.g.
Pintypealias Alias = P
Source compatibility
On its own, this change is not source-breaking. This change will be advantageous to programmers when Swift 6 rolls out. Swift 6 will enforce SE-0335 for all existential types, blocking the use of plain protocol syntax in any Swift codebase. The change laid out in this proposal will allow such code to compile in the supported positions and improve semantics instead of causing a compiler error.
Effect on ABI stability
This proposal has no impact on ABI stability.
Effect on API resilience
Packages and libraries that have not migrated to Swift 6 will still build with their existing language mode, so they will be unaffected by this change. However, when resilient libraries adopt Swift 6, they must adopt the any keyword in public API that uses bare protocol names today, because changing an existential type to a type parameter is a non-resilient change.
Alternatives considered
A common suggestion is to formalize the heuristics for when some is most valid and use those heuristics as rules for defaulting to some in some instances and defaulting to any in others. However, this model would re-introduce the conflation between the semantics of existential and opaque types, leading to a frustrating developer experience in the face of refactoring and code evolution. Seemingly harmless changes, such as factoring a local variable into a stored property, would end up changing the type of values unexpectedly and cause programmers to re-structure code that is unrelated to the refactoring task at hand, such as moving code that operated on that local variable into a separate function accepting some.
Future Directions
- Implicitly open existentials in more places. For example, the language could default to
somefor local variable assignments from ananytype without a type annotation, where itās possible to open the existential value:futr
let observers: [any DataSourceObserver]
for observer in observers {
// The default for `observer` could be (some) `DataSourceObserver`,*
// which means the scope of this for-loop would be a context*
// where you have access to Self and associated types
observer.method() // where 'method' uses an associated type
}
- Allow coercions to opaque types, e.g.
value as some P - Allow
some P?as a shorthand forOptional<some P>for bothsomeandany