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
some
keyword 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 whatsome
really 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
any
versussome
? - Where are you able to change
any
tosome
(or insertsome
in 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-in
variables, 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 useany
instead ofsome
. - Local
var
s whose underlying type does not change (only the value changes). - Non-
inout
function and method parameters, andinout
parameters 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
@ViewBuilder
and 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, 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.
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
.