Swift 6.0 Evolution
Swift 6 brings 23 new proposals to the language, totalling 10,922 lines and 73,896 words. This is a digest of what you need to know as a developer. Get comfortable, this is a long read.
Proposals in Swift 6
Index
- 0220 Count(where:)
- 0270 Add collection operations on noncontiguous elements
- 0301 Package editor commands
- 0405 String initializers with encoding validation
- 0408 Pack iteration
- 0409 Access-level modifiers on import declarations
- 0410 Low-level atomic operations
- 0414 Region based isolation
- 0415 Function body macros
- 0416 Subtyping for keypath literals as functions
- 0417 Task executor preference
- 0418 Inferring sendable for methods and key path literals
- 0420 Inheritance of actor isolation
- 0421 Generalize effect polymorphism for
asyncsequence
andasynciteratorprotocol
- 0422 Expression macro as caller-side default argument
- 0423 Dynamic actor isolation enforcement from non-strict-concurrency contexts
- 0424 Custom isolation checking for serialexecutor
- 0426
Bitwisecopyable
- 0428 Resolve DistributedActor protocols
- 0430
sending
parameter and result values - 0431
@isolated(any)
function types - 0432 Borrowing and consuming pattern matching for noncopyable types
- 0434 Usability of global-actor-isolated types
- 0435 Swift language version per target
- Conclusion
0220 count(where:)
0220 count(where:) implements exactly that.
0270 Add Collection Operations on Noncontiguous Elements
0270 adds RangeSet
to refer to discontinuous ranges of elements.
RangeSet
has set algebra methods (union, intersection, …) and aranges
variable returning its collection of ranges.Collection
andMutableCollection
have methods to work withRangeSet
.- New collection
DiscontiguousSlice
.
0301 Package Editor Commands
0301 adds commands to edit packages from the terminal. Example:
0405 String Initializers with Encoding Validation
0405 adds a new String
failable initializer.
0408 Pack Iteration
A pack is a list of zero or more types or values.
- Value parameter pack is a pack of values used as parameter.
- Type parameter pack is a pack of types used as parameter, where each T may be a different type.
Syntax
<each T>
, named Type Parameter Pack, is used as a generic type declaration.
T
is called pack nameeach T
is called repetition patterneach
indicatesT
is a pack of types- T can optionally be constrained to a protocol (it would be
<each T: Protocol>
)
values: (repeat each T)
, named Value Parameter Pack is used to declare properties or function parameters.
var
is the parameter namerepeat
indicates multiple valueseach T
, called repetition pattern, refers to the types in the pack
repeat expression(each T)
, named Pack Expansion Expression, expands a type or value pack within expressions.
each T
is called repetition pattern
After this word salad, let’s see an example to get an intuitive sense of what it all means.
Example
Example
Before SE-0408 I could pass arrays of different elements but only as a common supertype/protocol, and even then, I may still need the concrete type later. With type packs however, I can pass a ‘pack’ of different types.
Note that this wouldn’t work passing any View
:
Relevant links
- SE-0393 Value and Type Parameter Packs
- SE-0398 Allow Generic Types to Abstract Over Packs
- SE-0399 Tuple of value pack expansion
0409 Access-level modifiers on import declarations
0409 enhances the import
keyword so it can be prefixed with
public
. Visible to all clients.package
. Visible inside modules of the same package (e.g. a library target and its test module).internal
. Visible inside this module.fileprivate
orprivate
. Import is scoped to the source file declaring the import.
Example:
Upcoming feature flag: InternalImportsByDefault
.
0410 Low-Level Atomic Operations
0410 is implemented in a new Synchronization framework that adds the following:
- Lock-free types with well-defined semantics.
- Explicit memory ordering arguments for atomic operations, which provide guarantees on visibility to other threads.
- Basic operations (compare, load, store, add, etc.) for ints, pointers, floats, bool, and few other types. See UML.
Understanding this proposal requires knowing a few terms related to multi-threaded programming:
- Visibility is the ability of a thread to see the effects of write memory operations performed by other threads.
- Memory ordering are the rules for the order and visibility of memory operations (read and/or write operations) across multiple threads.
- Release is an operation in where a thread marks its updates to shared memory as complete and available to other threads.
- Acquire is an operation where a thread ensures it observes all prior modifications to shared memory that were released by other threads before proceeding.
These are the memory ordering arguments:
.relaxed
: Guarantees that this operation is atomic..acquiring
: Guarantees that all reads or writes to shared memory that occur after this operation will see the effects of previous releases on other threads. Used in read operations to ensure it sees the effect of previous writes..releasing
: Guarantees that all reads and writes performed by the current thread are completed before the release. Used in write operations to guarantee that a subsequent acquire sees its effects..acquiringAndReleasing
: Acquires and releases. Used by read-modify-write operations..sequentiallyConsistent
: Guarantees that concurrent operations performed in a value of shared memory are seen as happening in the same order by all threads.
Example
Add one million ten times, with each million executing potentially from a different thread. GCD decides how many threads to run, up to but not exceeding 10.
0414 Region based Isolation
0414 enhances the compiler ability to spot cases where passing a non-sendable between boundaries is actually safe. This eliminates a few unnecessary compiler warnings.
For instance, actor A passes a UIImage to actor B, who returns a copy of the image but doesn’t change the original image. Now the compiler is able to see that there are no escaping references, changes, and that the original image is no longer used in its original context.
If this example is confusing to see it may be time to review a few basic concepts related to Sendable.
Sendable
- A Race Condition occurs when the behavior of software becomes dependent on the sequence or timing of uncontrollable events, such as the execution order of multiple threads. It typically happens when multiple threads access shared data and at least one thread modifies the data.
- Mutual Exclusion is a mechanism that prevents several threads from concurrently accessing a shared value. That is, only one thread has access to state during a write.
- A Task in Swift is a self-contained unit of execution designed to handle asynchronous operations. It executes code sequentially and can only run on one thread at a time, though it may switch threads after reaching suspension points—places in the code where execution can be paused and later resumed. Tasks are sometimes called boundaries of execution.
- A Sendable value is one that can be passed between threads without violating thread-safety. A value is sendable if its state is immutable, or mutable but synchronized internally. If we send a non-Sendable value between boundaries (tasks), it may be modified in unpredictable ways, causing a race condition. For instance, imagine passing a counter where the read and increment operation is not atomic and is mixed with other read-and-increment operations.
0415 Function Body Macros
0415 adds an @attached(body)
macro that implements functions or adds code to their existing implementation. It can be applied to functions, initializers, deinitializers, and accessors.
Example use cases:
- Synthesizing remote procedure calls
- Adding logging/tracing
- Checking preconditions and invariants
- Replacing function bodies with new implementations
For instance,
This feature is still experimental so it needs:
and also
0416 Subtyping for keypath literals as functions
0416 improves the use of key paths as functions.
For instance, in the example below (Derived) -> Base
expects a result of type Base
, but the keypath returns a subclass of Base
. This is allowed for functions, and thanks to this proposal it is now also allowed for key paths.
This is related to SE-0249, introduced in Swift 5.2. 0249 added the ability to use key paths as functions when the match was exact. This meant there were cases where we couldn’t replace functions with key paths, because functions accept contravariant (wider) parameters and may return covariant (narrower) results.
What is Contravariant and covariant?
0417 Task Executor Preference
0417 adds API to specify a preferred executor for tasks and asynchronous functions. So the logic to choose an executor is as follows:
Executor preference API
- withTaskExecutorPreference(_:isolation:operation:)
- Task.init(executorPreference:priority:operation:)
- Task.init(executorPreference:priority:operation:) (throwing version)
- TaskGroup.addTask(executorPreference:priority:operation:)
- TaskGroup.addtaskunlesscancelled(executorpreference:priority:operation:)
What is an executor?
- A default “global concurrent executor” for any work that don’t specify executor requirements. This is the case for actors, tasks, and asynchronous functions without a preference.
- A SerialExecutor that executes tasks serially (one after another). This provides isolation for actors.
- A TaskExecutor that executes tasks serially (one after another). This is just a source fo threads for execution.
Executor preference is inherited by
async let
,- default actors (those without custom executors),
- TaskGroup
addTask()
(and variants like ThrowingTaskGroup, etc.).
Executor preference is NOT inherited by
Task {}
Task.detached {}
Global variable globalConcurrentExecutor returns the global concurrent executor. Variable unownedTaskExecutor will unsafely return the executor you are in.
unownedTaskExecutor
0418 Inferring Sendable for methods and key path literals
0418 improves how Swift handles Sendable
conformance for methods and key path literals. Main takeaway: less compiler complaints. You’ll have to write less @Sendable
annotations.
Enables automatic Sendability inference for certain methods and functions:
- Unapplied and partially applied methods of Sendable types.
- Non-local functions: These are functions defined at the global scope or as static methods, but not within other functions.
- Example.
Sendability for functions and methods
Disables automatic Sendability inference for key path literals. Instead:
- Key paths without non-
Sendable
captures will be inferred asSendable
when needed. - Developers can explicitly mark key paths as
Sendable
using& Sendable
. - Example.
Sendability in key path literals
Adds a compiler error for methods of non-Sendable
types that have been marked @Sendable
. Instead, you have to make the entire type Sendable. Example.
Methods of non-Sendable types can’t be marked @Sendable
-enable-experimental-feature InferSendableFromCapture
but doesn’t. Needs investigation.
0420 Inheritance of actor isolation
0420 lets functions opt-in to inherit the actor isolation of their caller. This is accomplished adding an isolated parameter with a default #isolation
value. Example:
The rules are:
- If called from a non-isolated context, isolation will be nil.
- If called from an actor method, isolation will be that actor.
- If called from a context isolated to a global actor (like
@MainActor
), isolation will be the shared instance of that global actor (e.g.MainActor.shared
).
Recap
So far an easy to understand proposal. However, so many language changes are starting to pile up. Let’s review function isolation and Sendability.
Function isolation in Swift refers to the context in which a function is allowed to execute and what data it can safely access. Currently there are:
- Functions non-isolated. Callable from any context, can’t access actor state.
- Functions isolated to a specific actor. Callable from that actor instance, can only access that actor isolated state.
- Functions isolated to a global actor. This can be inferred by the use of an annotation or by context (such as in the methods of main-actor-isolated types).
- Functions with custom isolation (SE-0420). Can inherit isolation from its caller or have dynamically determined isolation.
- Example.
Function isolation
Sendable functions, while not an isolation type, have restrictions on what these functions can capture and work with.
- They can be called from any context, including across actor boundaries.
- They must only capture
Sendable
data. This means:- Value types that are
Sendable
(structs, enums without associated references) - Classes marked as
@Sendable
orfinal class
with onlySendable
stored properties - Other
Sendable
functions
- Value types that are
- They cannot capture mutable state or non-
Sendable
references. - They don’t have special access to actor-
isolated
state. - When used as closure types, they enforce these rules at the point of closure creation.
- Example.
Sendable functions
0421 Generalize effect polymorphism for AsyncSequence and AsyncIteratorProtocol
Changes in 0421:
- Both protocols now have a Failure associated type that enable typed throws.
- AsyncIteratorProtocol has a new
next(isolation actor: isolated (any Actor)?)
method –explained in 0420. To avoid breaking existing source with that newnext()
method, there is a default implementation in Swift 6.
As of 07/08/24 seems typed throws is not ready yet. See Where is FullTypedThrows?.
0422 Expression macro as caller-side default argument
Thanks to 0422, expression macros can now be used as caller-side default arguments for functions.
I’ll explain,
- Default arguments for functions are usually evaluated in the function definition. Example:
func line(line: Int = 0) {}
. - However, using
#line
as argument passes the line of source code where the function was called. Example:func line(line: Int = #line) {}
. - Thanks to this proposal expression macros are expanded with caller-side source location information and context. Macro authors can use
context.location(of:)
to access source location information.
0423 Dynamic actor isolation enforcement from non-strict-concurrency contexts
This proposal enhances the ability of the Swift compiler to detect concurrency issues in dependencies imported using @preconcurrency import
. This feature primarily targets dependencies that haven’t implemented strict concurrency checking, and therefore, can’t be checked statically. Instead, the compiler will employ dynamic (= at runtime) checks on them.
As a developer, you don’t need to take any specific action. However, you may encounter additional runtime errors that highlight previously undetected data races. These errors actually improve your code’s safety by exposing concurrency issues that were previously silent.
This feature is gated behind the upcoming feature flag DynamicActorIsolation
.
0424 Custom isolation checking for SerialExecutor
0424 introduces a new protocol requirement called checkIsolated() for SerialExecutor. This allows custom executors to provide their own logic for safety checks like assertIsolated
and assumeIsolated
.
Details
- This is a runtime check that verifies whether the current execution context is isolated to a specific actor.
- It’s typically used for debugging and testing purposes.
- If the assertion fails (i.e., the code is not running in the expected isolated context), it triggers a runtime error.
- It doesn’t affect release builds, as assertions are typically stripped out in those.
assumeIsolated
- This is a more powerful variant that allows access to actor-isolated state.
- It takes a closure as an argument and executes that closure with the assumption that the current context is isolated to the actor.
- If the assumption is incorrect (i.e., the code is not actually isolated), it results in a runtime crash.
- Unlike
assertIsolated
, it affects both debug and release builds. - It’s used when you need to access actor-isolated state in a context where the compiler can’t statically verify isolation.
Example.
Sendable functions
0426 BitwiseCopyable
Updated: see see Copyable Types > BitwiseCopyable.
0428 Resolve DistributedActor protocols
I skipped this proposal because I have zero experience with IPC and distributed actors.
0430 sending parameter and result values
0430 adds a sending
keyword to indicate that a parameter or result value can be safely sent across isolation boundaries or merged into actor-isolated regions. This extends the concept of region isolation introduced in SE-0414, allowing non-Sendable typed values to be safely sent over isolation boundaries. Confused so far?
Fancy speaking
In the context of this proposal, there are three regions to consider, each with its scope of execution and local state: synchronous code, tasks, and actors.
Tasks and actors are isolated in the sense that their local state can’t be freely accessed:- A task doesn’t have properties or methods that can be accessed but can capture values when defined or send values explicitly starting other tasks or calling methods.
- An actor manages its own state with by queuing up calls to prevent concurrent execution.
- An isolated region is the scope of a task or actor.
- Crossing an isolation boundary is sending a value to or from a region, which is any combination of sync code/task/actor to sync code/task/actor (except sync code to sync code which is non isolated).
- Merging a value into an actor-isolated region is storing that value in the actor.
Requirements:
sending
parameters require the argument to be in a disconnected region at the call site. Meaning, when you call the function passing this parameter, there are no other references to this parameter that could lead to concurrent modifications. If I create the value just to pass it to you, no one else can touch it.sending
results require the function to return a value in a disconnected region. Meaning, the value should be freshly created within the function or otherwise detached from any shared state that could lead to concurrent access.
This example sends and return a non referenced value, therefore safe. The compiler would have emitted an error if the value was stored.
sending
can be combined with inout
and ownership modifiers like consuming
and borrowing
.
0431 @isolated(any) Function Types
0431 introduces a new @isolated(any)
attribute for function types in Swift. A function value with this type dynamically (at runtime) carries the isolation of the function that was used to initialize it.
Example:
- If the function is dynamically non-isolated, the value of isolation is nil.
- If the function is dynamically isolated to a global actor type G, the value of isolation is G.shared.
- If the function is dynamically isolated to a specific actor reference, the value of isolation is that actor reference.
When called from an arbitrary context, these functions are assumed to cross an isolation boundary, requiring await. Example:
0432 Borrowing and consuming pattern matching for noncopyable types
Updated: see Copyable Types > Pattern Matching.
0434 Usability of global-actor-isolated types
0434 lifts unnecessary restrictions. Below, I quote in bold the proposal changes.
Stored properties of Sendable type in a global-actor-isolated value type are implicitly nonisolated, and can be optionally declared as nonisolated without using (unsafe) to be treated as such outside the defining module. This is because all global actors serialize access to their members, and because the value is safe to send across boundaries. The benefit is that synchronous code can access the members while still respecting the thread-safety guarantees.
Note that wrapped properties and lazy-initialized properties with Sendable type still must be isolated because they are computed properties, not stored values.
@Sendable is inferred for global-actor-isolated functions and closures. Example:
Global-actor-isolated closures are allowed to capture non-Sendable values despite being @Sendable. The above code is data-race safe, since a globally-isolated closure will never operate on the same instance of NonSendable concurrently. Example:
Permit global-actor-isolated subclasses of non-isolated, non-Sendable
classes, but prevent them from being Sendable
. Subclasses may add global actor isolation when inheriting from a nonisolated, non-Sendable superclass. In this case, an implicit conformance to Sendable will not be added, and explicitly specifying a Sendable conformance is an error. This is because an overridden method does not inherit the isolation of its type, instead, it preserves the isolation of the superclass so it can be usable anywhere the superclass is usable.
This proposal is gated behind upcoming feature flag GlobalActorIsolatedTypesUsability
.
0435 Swift Language Version Per Target
0435: SPM Targets can specify a language version to facilitate migration.
Conclusion
Recap of the recap: how the language has evolved?
- Fine grained control in memory (
sending
) and performance (atomics and preferred executors). - Powerful metaprogramming (side caller context on macros) and generics (value and type packs).
- The usual ergonomic changes like
count(where:)
(0220), string initializers (0405), improvements to key paths as functions.
and finally the big one
- Safer code with less warnings. 9 out of 23 proposals enhance the compiler reasoning about isolation contexts, sendable inference, and
@preconcurrency
code. For instance, now we can pass non-sendables for read-only purposes without irritating warnings.
Concurrency proposals
- 0414 Region based isolation
- 0417 Task executor preference
- 0418 Inferring sendable for methods and key path literals
- 0420 Inheritance of actor isolation
- 0423 Dynamic actor isolation enforcement from non-strict-concurrency contexts
- 0424 Custom isolation checking for serialexecutor
- 0430 `sending` parameter and result values
- 0431 `@isolated(any)` function types
- 0434 Usability of global-actor-isolated types
Reading this took some time and effort. But the main benefit is exactly that exercise, reasoning to polish my mental model. Despite some new keywords I think the language got easier. Overall I’m glad for all these changes.
For those skipping the reading, Swift will silently work better. Type packs is the one that’s going to confuse people who first see it, but you get the intuition right with just one example. Similar with sending
, @isolated
, and preferred executors. I think we are all better with Swift 6.