Swift 5.7 includes a suite of new language features that together enhance the expressivity and usability of any and some types, making abstract code more approachable and easier to internalize.
In addition to making a lot of common generic code patterns easier to write by eliding an explicit generic signature completely, Swift 5.7 makes it very easy to switch between any types and some types. 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.
The any keyword will be required in Swift 6, which opens an opportunity to change the default when writing a plain protocol name to some instead of any. This post explores the motivation and implications for making such a change.
Feedback from your hands-on experience internalizing and applying the new generics features in Swift 5.7 is welcome and crucial for informing this exploration! In particular, I'm interested to know things such as:
- Whether you find the
somekeyword in opaque result types more clarifying now that (hopefully) programmers are starting to gain a better understanding of opaque result types. Pre-5.7, I'd received a lot of feedback that people don't know whatsomereally means and they just stick it into their SwiftUI views to satisfy the compiler. - What kinds of code are you able to modernize in Swift 5.7?
- How often do you use
anyversussome? - Where are you able to change
anytosome(or insertsomein front of a bare protocol name in existing code)?
Use cases for some and any
The some and any keywords have different capabilities, and will be useful in different contexts in a project.
some guarantees a fixed underlying type, which preserves static type relationships to that underlying type, giving you full access to the protocol requirements and extension methods that use Self and associated types. These are the semantics that programmers typically expect when working with protocols, and have many use cases in abstract code, including (but not limited to):
- Local
let-constants, includingfor-invariables, e.g. when iterating over a heterogeneous collection. Because the variable cannot be re-assigned to have a different underlying type, thereâs little reason to useanyinstead ofsome. - Local
vars whose underlying type does not change (only the value changes). - Non-
inoutfunction and method parameters, andinoutparameters whose underlying type does not change (only the value changes). - Single-statement computed property or method result types, and result builder bodies that preserve type information such as
@ViewBuilderand similar DSLs.
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 that have 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.
For any types, where the underlying type can vary, the protocol requirements and methods involving Self and associated types are not available from the box itself; 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 signature of a requirement using Self or associated types in contravariant or invariant position is. For this reason, programmers are encouraged to write some by default, and change some to any when the storage flexibility is needed. This workflow is similar to writing let constants by default until mutation is needed.
Should some be the default in Swift 6?
Encouraging writing some by default begs the question of whether some should be the default for a plain protocol name in the language, just like how let is the default whenever the variable introducer is omitted, such as in parameters and for-in loops. In Swift 6, a plain protocol name P could still be valid code, but it would mean some P instead of any P.
Writing any explicitly is important for understanding the type-erasing semantics and the limitations of using this type, including the ability to change the underlying type, the inability to access Self and associated type requirements, etc. These limitations donât exist for some types, which provide full access to the protocol requirements and extension methods. In fact, some already is the default for protocol extensions, e.g.
// Extends all concrete types conforming to 'Collection'.
// The 'Self' type parameter serves as a placeholder for the
// concrete type conforming to 'Collection'.
extension Collection { ... }
In a protocol extension, you write generic code that operates on a Self type parameter that conforms to the protocol. In this case, the bare protocol name is already sugar for declaring a type parameter conforming to the protocol. Protocol extensions have proven to be a very natural way to write generic code. The same principle could be applied to plain protocol names in Swift 6 to enable programmers to write generic functions naturally without having to fully internalize explicit generic signatures with where clauses.
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 Swift 5.7, 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)>
In addition to eliding the some keyword in type annotations, the language could default to some in more places, such as local variable assignments to an any type without a type annotation, where itâs possible to open the existential value:
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
}
Observations and open questions
Brainstorming this idea has surfaced a number of observations that are worth discussing.
First, there are a few cases where the some keyword could be considered clarifying. For example, the some in [some Comparable] communicates that the array is homogeneous. Similarly, in var value: some P = ConcreteP(), the some keyword conveys that the underlying type cannot change.
Composing type parameters with Optional is tricky to work with, because passing nil doesnât provide a generic argument type, so replacing any with some for optional function parameters might not be as straightforward as changing the keyword. One of the main use cases of optional any parameters could be changed to use SE-0347 instead, and provide a concrete default argument instead of nil.
Eliding some is not possible when only using a superclass constraint. There isnât much benefit to writing some ClassType over using the class type directly, so this is probably okay.
An obvious downside of changing the semantics of existing syntax in Swift 6 is that numerous resources, such as Swift Forum questions and blog posts, have been created over the years using the existing syntax. This content will become outdated with Swift 6, where previously-existential code would have different semantics. However, programmers will already need to evolve their understanding of any types with SE-0309 in Swift 5.7, which introduces the need to reason about uses of associated types in protocol requirements when working with any types. So, content that features existential types will already need to be updated to apply to Swift 6, and thereâs a fair amount of existing existential code that could flip to using generics and still behave correctly, as mentioned earlier.
Finally, because a lot of existing code can switch from any to some and still behave correctly, eliding some may ease the transition to Swift 6 by requiring less code churn, and even improving the semantics of some existing Swift code.
Alternatives considered
In this post, Iâve outlined the common use cases for some and any. A common suggestion is to formalize the heuristics for when some is most useful versus when any is most useful, and use those heuristics as rules for defaulting to some in certain cases and defaulting to any in others. However, this model would re-introduce the conflation between the semantics of existential types and opaque types, and it would lead to a frustrating developer experience in the face of refactoring and code evolution. Innocuous 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 seemingly unrelated to the refactoring task at hand, such as moving code that operated on that local variable into a separate function accepting some.