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
main
behind 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 andvar
s 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.
P
instruct S: P {}
- Generic conformance requirements, e.g.
P
infunc test<T>() where T: P
- The right hand side of a type-alias declaration, e.g.
P
intypealias 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
some
for local variable assignments from anany
type 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 bothsome
andany