What's New in Swift 5.9

Updated Jun 14, 2023#swift-versions#swift

Swift has a formal process for proposing and accepting changes to the language known as the Swift Evolution Process. This process encompasses all aspects of the language, including language features, standard library, compiler configuration, and package manager.



Swift 5.9, which is included in Xcode 15 Beta, incorporates over 11 implemented proposals that are currently behind experimental flags. It is important to note that further modifications and additions are expected before the official release.

  1. SE-0388Convenience Async[Throwing]Stream.makeStream methods

This 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)
    }
  }
}
  1. SE-0380if and switch expressions

This feature introduces the ability to use if and switch statements as expressions, for the purpose of:

  • Returning values from functions, properties, and closures (either implicit or explicit)
  • Assigning values to variables
  • Declaring variables
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:

  • Each branch of the if, or each case of the switch, must be a single expression.
  • Each of those expressions, when type checked independently, must produce the same type.
  • In the case of if statements, the branches must include an else.
  • The expression is not part of a result builder expression.
  • Pattern matching bindings may occur within an if or case.
  1. SE-0384Importing forward declared Objective-C interfaces and protocols

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.

  1. SE-0381DiscardingTaskGroups

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:

  • They automatically cleans up its child Tasks when those Tasks complete.
  • They do not have a 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)
        }
    }
}
  1. SE-0374Add sleep(for:) to Clock

There 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
  1. SE-0392Custom Actor Executors

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.

  1. SE-0382Expression Macros

This proposal introduces the notion of expression macros, which are used as expressions in the source code (marked with #) and are expanded into expressions. Expression macros can have parameters and a result type, much like a function, which describes the effect of the macro expansion on the expression without requiring the macro to actually be expanded first.

As a simple example, let’s consider a stringify macro that takes a single argument as input and produces a tuple containing both the original argument and also a string literal containing the source code for that argument. The type signature of a macro is part of its declaration, which looks a lot like a function.

@freestanding(expression) macro stringify<T>(_: T) -> (T, String)

This macro could be used in source code as:

#stringify(x + y)

and would be expanded into:

(x + y, "x + y")

Macro arguments are type-checked against the parameter types of the macro prior to instantiating the macro. If they are ill-formed, the macro will never be expanded.

  1. SE-0389Attached Macros

Attached macros provide a way to extend Swift by creating and extending declarations based on arbitrary syntactic transformations on their arguments. They make it possible to extend Swift in ways that were only previously possible by introducing new language features, helping developers build more expressive libraries and eliminate extraneous boilerplate.

Attached macros are identified with the @attached attribute, which also provides the specific role as well as any names they introduce. For example, the aforemented macro to add a completion handler would be declared as follows:

@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")

The macro can be used as follows:

@AddCompletionHandler(parameterName: "onCompletion")
func fetchAvatar(_ username: String) async -> Image? { ... }

The use of the macro is attached to fetchAvatar, and generates a peer declaration alongside fetchAvatar whose name is overloaded with fetchAvatar. The generated declaration is:

/// Expansion of the macro produces the following.
func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) {
  Task.detached {
    onCompletion(await fetchAvatar(username))
  }
}
  1. SE-0397Freestanding Declaration Macros

The proposal extends the notion of freestanding macros introduced in expression macros to also allow macros to introduce new declarations. Like expression macros, freestanding declaration macros are expanded using the # syntax, and have type-checked macro arguments. However, freestanding declaration macros can be used any place that a declaration is provided, and never produce a value.

The #warning can be described as a freestanding declaration macro as follows:

/// Emits the given message as a warning, as in SE-0196.
@freestanding(declaration) 
macro warning(_ message: String) = #externalMacro(module: "MyMacros", type: "WarningMacro")

and can be used anywhere a declaration can occur:

#warning("unsupported configuration")
  1. SE-0394Package Manager Support for Custom Macros

This proposal covers how custom macros are defined, built and distributed as part of a Swift package. SwiftPM builds each macro as an executable for the host platform, applying certain additional compiler flags. Macros are expected to depend on SwiftSyntax using a versioned dependency that corresponds to a particular major Swift release.

Similar to package plugins, macro plugins are built as executables for the host (i.e, where the compiler is run). The compiler receives the paths to these executables from the build system and will run them on demand as part of the compilation process. Macro executables are automatically available for any target that transitively depends on them via the package manifest.

A minimal package containing the implementation, definition and client of a macro would look like this:

import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MacroPackage",
    dependencies: [
        .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"),
    ],
    targets: [
        .macro(name: "MacroImpl",
               dependencies: [
                   .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                   .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
               ]),
        .target(name: "MacroDef", dependencies: ["MacroImpl"]),
        .executableTarget(name: "MacroClient", dependencies: ["MacroDef"]),
        .testTarget(name: "MacroTests", dependencies: ["MacroImpl"]),
    ]
)

If the above macro package contains definition and implementation of the #fontLiteral macro, which is similar in spirit to the built-in expressions #colorLiteral, #imageLiteral, etc. You can use it like this:

import MacroDef

struct Font: ExpressibleByFontLiteral {
  init(fontLiteralName: String, size: Int, weight: MacroDef.FontWeight) {
  }
}

let _: Font = #fontLiteral(name: "Comic Sans", size: 14, weight: .thin)
  1. SE-0393Value and Type Parameter Packs

This proposal adds type parameter packs and value parameter packs to enable abstracting over the number of types and values with distinct type. This is the first step toward variadic generics in Swift.

Parameter packs are the core concept that facilitates abstracting over a variable number of parameters. A pack is a new kind of type-level and value-level entity that represents a list of types or values, and it has an abstract length.

A type parameter pack stores a list of zero or more type parameters, and a value parameter pack stores a list of zero or more value parameters. A type parameter pack is declared in angle brackets using the each contextual keyword:

A pack expansion is a new kind of type-level and value-level construct that expands a type or value pack into a list of types or values, respectively. Written as repeat P, where P is the repetition pattern that captures at least one type parameter pack (spelled with the each keyword). At runtime, the pattern is repeated for each element in the substituted pack.

The following example demonstrates these concepts together:

struct Pair<First, Second> {
  init(_ first: First, _ second: Second)
}

func makePairs<each First, each Second>(
  firsts first: repeat each First,
  seconds second: repeat each Second
) -> (repeat Pair<each First, each Second>) {
  return (repeat Pair(each first, each second))
}

let pairs = makePairs(firsts: 1, "hello", seconds: true, 1.0)
// 'pairs' is '(Pair(1, true), Pair("hello", 2.0))'