The Swift language has a formal process for proposing and accepting changes to the language, known as the Swift Evolution Process. Following proposals have been implemented and assigned to upcoming Swift 5.9. There will be more changes until official release.
Async[Throwing]Stream.makeStream
methodsThis feature adds a new static method makeStream
on AsyncStream and AsyncThrowingStream that returns both the stream and the continuation, which make the stream’s continuation easier to access.
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
await withTaskGroup(of: Void.self) { group in
group.addTask {
for i in 0...9 {
continuation.yield(i)
}
continuation.finish()
}
group.addTask {
for await i in stream {
print(i)
}
}
}
if
and switch
expressionsThis feature introduces the ability to use if
and switch
statements as expressions, for the purpose of:
let bullet =
if isRoot && (count == 0 || !willExpand) { "" }
else if count == 0 { "- " }
else if maxDepth <= 0 { "▹ " }
else { "▿ " }
public static func width(_ x: Unicode.Scalar) -> Int {
switch x.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default: 4
}
}
For an if
or switch
to be used as an expression, it would need to meet these criteria:
if
, or each case of the switch
, must be a single expression.if
statements, the branches must include an else
.if
or case
.Forward declarations are very common in many existing Objective-C code bases, used often to break cyclic dependencies or to improve build performance. As it stands, the ClangImporter will fail to import any declaration that references a forward declared type in many common cases. This means a single forward declared type can render larger portions of an Objective-C API unusable from Swift.
This feature proposes the following representation for forward declared Objective-C interfaces and protocols in Swift:
// @class Foo turns into
@available(*, unavailable, message: “This Objective-C class has only been forward declared; import its owning module to use it”)
class Foo : NSObject {}
// @protocol Bar turns into
@available(*, unavailable, message: “This Objective-C protocol has only been forward declared; import its owning module to use it”)
protocol Bar : NSObjectProtocol {}
Permitted usages of these types are intentionally limited. You will be able to use Objective-C and C declarations that refer to these types without issue. You will be able to pass around instances of these incomplete types from Swift to Objective-C and vice versa.
Using the type itself directly in Swift is forbidden by the attached unavailable attribute. This is to keep the impact of the change small and to prevent unsound declarations, such as declaring a new Swift class that inherits from or conforms to such a type. You will also not be able to create new instances of these types in Swift.
The feature is gated behind a new frontend flag -enable-import-objc-forward-declarations
. This flag is on by default for Swift version 6 and onwards.
[Throwing]DiscardingTaskGroup
This feature proposes discarding task group types:
DiscardingTaskGroup
, withDiscardingTaskGroup
ThrowingDiscardingTaskGroup
, withThrowingDiscardingTaskGroup
Similar to [Throwing]TaskGroup
, however it discards results of its child tasks immediately. It is specialized for potentially never-ending task groups, such as top-level loops of http or other kinds of rpc servers to avoid memory leaks and performance issues when running for a long time or with many child tasks.
Discarding task group types work as follow:
next()
method, nor do they conform to AsyncSequence
.Cancellation and error propagation of discarding task group works the same way one comes to expect a task group to behave, however due to the inability to explicitly use next()
to “re-throw” a child task error, the discarding task group types must handle this behavior implicitly by re-throwing the first encountered error and cancelling the group.
// GOOD, no leaks!
try await withThrowingDiscardingTaskGroup() { group in
while let newConnection = try await listeningSocket.accept() {
group.addTask {
handleConnection(newConnection)
}
}
}
sleep(for:)
to ClockThere is currently a slight imbalance between the sleep APIs for clocks and tasks, and it causes some troubles when dealing with Clock
existentials.
let clock = ContinuousClock()
// Instant-based
try await Task.sleep(until: .now.advanced(by: .seconds(1)), clock: .continuous)
try await clock.sleep(until: .now.advanced(by: .seconds(1))
// Duration-based
try await Task.sleep(for: .seconds(1))
try await clock.sleep(for: .seconds(1)) // 🛑 API does not exist
The real problem occurs when dealing with Clock existentials. Because the Instant associated type is fully erased, and only the Duration is preserved via the primary associated type.
let clock: any Clock<Duration> = ContinuousClock()
// Instant-based
try await clock.sleep(until: .now.advanced(by: .seconds(1)) // 🛑
This release introduce a sleep(for:)
method that only dealt with durations, and not instants, then we could invoke that API on an existential:
let clock: any Clock<Duration> = ContinuousClock()
// Duration-based
try await clock.sleep(for: .seconds(1)) // ✅ if the API existed
This proposal proposes a way to customize the executors of actors in Swift, which are responsible for scheduling and running tasks on actors. Executors are abstract types that conform to the Executor
protocol and provide a enqueue(_:)
method to submit tasks to be executed.
/// A service that can execute jobs.
public protocol Executor: AnyObject, Sendable {
// This requirement is repeated here as a non-override so that we
// get a redundant witness-table entry for it. This allows us to
// avoid drilling down to the base conformance just for the basic
// work-scheduling operation.
func enqueue(_ job: consuming ExecutorJob)
@available(*, deprecated, message: "Implement the enqueue(_:ExecutorJob) method instead")
func enqueue(_ job: UnownedExecutorJob)
}
This proposal also defines a SerialExecutor
protocol, which is what actors use to guarantee their serial execution of tasks (jobs).
/// A service that executes jobs one-by-one, and specifically,
/// guarantees mutual exclusion between job executions.
///
/// A serial executor can be provided to an actor (or distributed actor),
/// to guarantee all work performed on that actor should be enqueued to this executor.
///
/// Serial executors do not, in general, guarantee specific run-order of jobs,
/// and are free to re-order them e.g. using task priority, or any other mechanism.
public protocol SerialExecutor: Executor {
/// Convert this executor value to the optimized form of borrowed
/// executor references.
func asUnownedSerialExecutor() -> UnownedSerialExecutor
// Discussed in depth in "Details of 'same executor' checking" of this proposal.
func isSameExclusiveExecutionContext(other executor: Self) -> Bool
}
extension SerialExecutor {
// default implementation is sufficient for most implementations
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
func isSameExclusiveExecutionContext(other: Self) -> Bool {
self === other
}
}
All actors implicitly conform to the Actor
(or DistributedActor
) protocols, and those protocols include the customization point for the executor they are required to run on in form of the the unownedExecutor
property.