Swift

Getting back into it after a few years away. I began by reading the 2022-09-12 version of The Swift Programming Language. TODO go through my highlights of that on Kindle

Type system

WWDC 2022 110353 – Design protocol interfaces in Swift

I came into this wanting to know more about the any MyProtocol meaning, and also the meaning of “primary associated type”, neither of which are explained in TSPL but which I’d heard elsewhere (possibly in this talk at some other time).

Builds on “Embrace Swift generics” talk, will watch that after

Understand type erasure

“Let’s start by learning how protocols with associated types interact with existential types.”

Animal has an associated CommodityType that they .produce(), then we have a Farm with an array var animals: [any Animal].

“… the any Animal type has a box(ed?) representation that has the ability to store any type of animal dynamically”

“…the strategy of using the same representation for different concrete types is called type erasure

Now explains why animals.map { $0.produce() } returns [any Food]:

“The return type of produce() is an associated type. When you call a method returning an associated type on an existential type, the compiler will use type erasure to determine the result type of the call. Type erasure replaces these associated types with corresponding existential types that have equivalent constraints.”

The type any Food is called the upper bound of the associated CommodityType.

Associated types appearing in the result of a function declaration are said to be in a producing position. Associated types in producing position are type erased to their upper bound.

On the other hand, associated int he parameter list of a function declaration are said to be in the consuming position (e.g. eat(_: FeedType))

“type erasure does not allow us to work with associated types in consuming position. Instead you must unbox the existential any type by passing it to a function that takes an opaque some type.

Hide implementation details

We add var isHungry: Bool to Animal. We have Farm.feedAnimals() which feeds all the hungry animals. And a hungryAnimals property that initially returns [any Animal], but which we decide to optimise by returning animals.lazy.filter(\.isHungry) and hence returns LazyFilterSequence<[any Animal>]. But the client doesn’t care about this implementation detail.

So we can use an opaque result type to hide the concrete type. But if we just wrote var hungryAnimals: some Collection we’d be hiding too much type information - we wouldn’t know the collection’s Element type.

So we can use a constrained result type (new in Swift 5.7). Written by applying type arguments in angle brackets after the protocol name, e.g. some Collection<any Animal>.

This works because the Collection protocol declares that the Element associated type is a primary associated type, which is declared in angle brackets after the protocol name:

protocol Collection<Element>: Sequence {
	associatedtype Element
	...
}

“Often you’ll see a correspondence between the primary associated types of a protocol and the generic parameters of a concrete type conforming to this protocol” – e.g. Array<Element>, Set<Element>

And in Swift 5.7 we have constrained existential types (i.e. can now do any Collection<Element>.)

Identify type relationships

Imagine we have:

struct Cow: Animal {
	func eat(_: Hay) { ... }
}

struct Hay: AnimalFeed {
	static func grow() -> Alfalfa { ... }
}

struct Alfalfa: Crop {
	func harvest () -> Hay { ...
}

let cow: Cow = ...
let alfalfa = Hay.grow()
let hay = alfalfa.harvest()

cow.eat(hay)

and also similarly with Scratch, which we grow() to create Millet, which a Chicken can then eat(:_).

So, how will Farm’s feedAnimals() work? It’s going to loop through its hungryAnimals, and call feedAnimal(animal).

At that point, we need to “unbox the existential type”, which seems to mean something (?) happening to turn any Animal into some Animal.

private func feedAnimal(_ animal: some Animal)`

But what does that implementation look like?

(By the way, I didn’t see TSPL talking about using some in a producing position, but we’re seeing it here…)

So we do this:

let crop = type(of: animal).FeedType.grow()
let feed = crop.harvest()
animal.eat(feed)

(Is that type(of: animal) known at compile time?)

But apparently this doesn’t guarantee that we get back the same type of animal feed we started with.

Oh, looks like this is just getting at making sure we use a same-type requirement with a where clause:

associatedtype CropType: Crop where CropType.FeedType === Self

(I think this is nothing new, might have missed something here.)

WWDC 2022 110352 - Embrace Swift generics

This seems to be building up the thing from before of growing crops, which we then harvest, which we then feed to multiple animals. Starts with concrete example with a cow etc, and then shows how generics allow us to model this.

Says that this is a really common pattern:

func feed<A>(_ animal: A) where A: Animal

which can now be expressed (with identical meaning) in terms of the protocol conformance as

func feed(_ animal: some Animal)

(I think this is new in Swift 5.7.)

The specific underlying type that is substituted in to an opaque type is called the underlying type.

Local variables with opaque type must always have an initial value.

Think of any Animal as a box, formally called an existential type.

How do we call animal.eat(_:) on an any Animal? We need to unbox it – i.e. convert an instance of any Animal to some Animal by unboxing the existential type. “Opening type-erased boxes”. New in Swift 5.7. (See the full implementation, using type(of: animal in the talk I described above.)

In general, write some by default.

Concurency

WWDC 2021 10132 - Meet async/await in Swift

Basics

I think this is a concept that is more simple than structured concurrency (that is, Tasks and the language’s automatic management of their hierarchy, and the ability to manage a group of them). It’s just the idea that a function is able to suspend its execution (that is, give up control of a thread but returning control to the system instead of the caller) for the system to resume it later. This is a concept which can be informally implemented via e.g. completion handlers, but then the language is unable to enforce things that it does for synchronous code, such as that a function either returns a value or throws an error. Notice also that this has nothing to do with actors, which although integrated with the concurrency system, are simply a means to protect mutable state.

Notice that SE-0296 – Async/await says:

Because only async code can call other async code, this proposal provides no way to initiate asynchronous code. This is intentional: all asynchronous code runs within the context of a “task”, a notion which is defined in the Structured Concurrency proposal.

Read-only can be async too, e.g. await maybeImage?.thumbnail. They need an explicit getter so they can be marked get async. Initializers can be async too.

await also works in for loops for iterating over async sequences. See the “Meet AsyncSequence” section.

When a function resumes, there’s no guarantee it’ll do so on the same thread as before.

Testing

XCTest supports async test functions out of the box — removing the need for expectations.

(They don’t address what to do if you’d like a timeout.)

Bridging from sync to async

How do you call async code in a context that doesn’t support concurrency? This is where a taste of Task comes in.

Task {
  // something
}}

Existing APIs that take completion handlers

The compiler provides async alternatives to Objective-C methods that take completion handlers.

Furthermore, given delegate or data source methods that receive a completion handler (which are used to inform the framework when some work has completed), the compiler creates an async alternative.

We recommend that async functions omit leading words like get, that communicate when the results of a call are not directly returned.

Async alternatives and continuations

To use a callback method inside an async function? Use withCheckedThrowingContinuation. The continuation has resume(throwing:) and resume(returning:) methods.

A continuation must be resumed exactly once – < 1 results in a warning, > 1 results in a fatal error.

WWDC 2021 10058 – Meet AsyncSequence

I thought perhaps it would be interesting to look at this next instead of going on to structured concurrency and actors, since, just like async/await, it’s just another extension of existing synchronous programming ideas.

Unlocks the for (try) await syntax.

AsyncSequence has all the expected Sequence functionality like map, reduce, dropFirst().

AsyncSequence will suspend on each element, and resume when the underlying iterator produces a value or throws.

Unlike Sequence, they can throw an error.

I don’t understand what happens if multiple people share an async sequence. Need a deeper dive into the documentation on this one.

Bear in mind that it’s quite common for an async sequence not to terminate. (You might, for example, want to wrap the iteration in a separate Task in that case. That way, you can also externally cancel the iteration by cancelling the task. Presumably, iteration has no special handling of cancellation and you still need to cooperate and check for it.)

Iteration is not the only way to interact with an AsyncSequence

For example, you could await center.notifications(named: …).first { … }.

System frameworks

How can I make my own AsyncSequence?

Let’s see how to adapt existing patterns such as (callbacks / delegates) that are executed multiple times, which don’t require any response back.

Our bridge to the AsyncSequence world is AsyncStream, which vends a continuation with a yield function (and also an onTermination which they just say “handles termination and cleanup” – one to understand better)

let quakes = AsyncStream(Quake.self) { continuation in
	let monitor = QuakeMonitor()
	monitor.quakeHandler = { quake in
		continuation.yield(quake)
	}
	continuation.onTermination = { _ in
		monitor.stopMonitoring()
	}
	monitor.startMonitoring()
}

So, something that’s not clear to me is what the consequences are of the fact that simply creating this stream starts some work happening. Is that different to the way things work in Combine / Rx? I don’t know enough about those models either to have much to say on this yet though.

Sounds like there’s other stuff to understand about AsyncSequence, like how it handles buffering (and what that even means).

There’s also AsyncThrowingStream if you want to be able to throw errors.

Language

I’m not sure what the correct language is that corresponds to concepts like “publish an element”, if I wanted to document an API.

Time

I think — from seeing one of the Swift Evolution proposals — that async sequences are bound to some concept of time and of representing it. But that wasn’t mentioned here. Another one to look into.

WWDC 2022 110355 – Meet Swift Async Algorithms

Sounds like this is the package that introduces interaction with Clock.

Multi-input algorithms

Combining async sequences into a single output

  1. Zip

    Combines values into tuples. Rethrows errors. Each side is waited concurrently (i.e. one doesn’t block the other)

  2. Merge

    Combine multiple AsyncSequences into one AsyncSequence

    If any of the iterations produces an error, the other iterations are cancelled.

New Swift APIs for handling time

There are some APIs for leveraging the new Clock, Instant and Duration in Swift 5.7. Let’s first understand what these types are.

Algorithms for handling time (in Async Algorithms package)

  1. Debounce – debounce(for: .milliseconds(300))

    Awaits a quiescence period to produce events. Rethrows failures immediately. Used for example for rate limiting searches when user enters input

  2. Chunks – groups elements into collections by count, time, or content

    e.g. let batches = outboundMessages.chunked(by: .repeating(every: .milliseonds(500))

    Used e.g. for batching requests to the server

Collection initializers

Initialize Dictionary, Set or Array from (known finite) async sequences

WWDC 2021 10134 — Explore structured concurrency in Swift

Structured programming is an idea we take for granted these days – it makes control flow more uniform. For example, it ties together control flow and variable lifetime.

Asynchronous code with completion handlers is unstructured. (I think that some of this we’ve already seen in the async/await talk – unable to throw errors, unable to use loops).

Let’s continue beyond what we saw in the async/await talk. We’ve seen how to use async/await to generate thumbnails sequentially in a loop. But want if we want to do it concurrently, so that multiple downloads can happen in parallel?

Tasks

You can create additional tasks to add concurrency to a program. A task provides a fresh execution context to run asynchronous code. Each task runs concurrently with respect to other execution contexts.

They’ll be automatically scheduled to run in parallel when safe and efficient to do so.

Calling an async function does not create a task for the call. You create tasks explicitly.

Task hierarchy and cancellation

The task tree

Tasks form part of a hierarchy called a task tree.

A parent task can only finish its work if all of its child tasks have finished. This guarantee is fundemental to structured concurrency. It prevents you from # accidentally leaking child tasks.

Cancellation and hierarchy

When a task is cancelled, all subtasks that are descendents of that task will be automatically cancelled too.

Cancellation is cooperative

Tasks are not stopped immediately when cancelled. They must check if they have been cancelled.

You can check for cancellation from anywhere, async or not. Design your code with cancellation in mind.

You can use try Task.checkCancellation(), which throws an error if the task is cancelled. You can also use if Task.isCancelled.

You might wish to return a partial result if a task is cancelled. If doing so, you must document this.

Flavours of tasks

They fall into two categories:

Here’s a summary from the end of the talk:

table of Swift task types

Async-let tasks

async let result = URLSession.shared.data(…) – this creates a new task, a child of the task that created it.

You then need to try [await] result.

If one async let task fails before another one is awaited, Swift will automatically mark the un-awaited task as cancelled, and then await for it to finish, before ending the function.

Group tasks

Provides a dynamic amount of concurrency. Use withThrowingTaskGroup.

Tasks added to a group cannot outlive the scope of the block in which the group is defined.

You add tasks to a group using group.async. Once added to a group, child tasks are executed immediately and in any order.

A task group will await all of its child tasks when it goes out of scope. If a child task fails, the other tasks in the groups will be cancelled.

Unstructured tasks

Not all tasks fit a structured pattern:

We use the Task { … } initializer. “Swift will schedule the task to run on the main actor as the originating scope” (?)

These tasks “inherit actor isolation and priority of the the origin context” (?), but the lifetime is not confined to any scope. We must manually handle the things that structured concurrency would have handled automatically — cancellation and awaiting the result.

In their example, they show how to kick off a task in response to a UICollectionView telling us it’s going to display a cell. They show that we can access the containing class’s mutable state (a dictionary) inside the task body without a compiler error, because “the delegate class is bound to the main actor (@MainActor), and the new task inherits that, so they will never run together in parallel”. And they then cancel the task when the cell goes out of view (another delegate callback).

Detached tasks

Also unstructured, but they don’t inherit anything from their originating context. In their example, they use it to write some thumbnails to a file. Use Task.detached { … }. You can pass e.g. (priority: .background).

Static concurrency checking

For example, if you try to modify a dictionary inside the body of a task group, you’ll get a compiler error “mutation of captured var ‘foo’ in concurrently-executing code”.

Task creation takes a closure of a new type @Sendable. Its body is not allowed to capture mutable variables. They should only capture value types, actors, or classes that implement their own synchronization.

See the session “Protect mutable state with Swift actors”.

In the particular example in this talk, they instead make each task return a value, and then use a for try await (id, thumbnail) in group (the group conforms to AsyncSequence) in order to store the values in a dictionary — this loop runs sequentially.

Swift Package Manager

WWDC 2022 110359 – Meet Swift Package plugins

A Swift script that can perform actions on a Swift package. Uses a special API provided for this purpose.

They’re themselves implemented as Swift packages. A plugin can use more than one source file. A Swift package can define more than one plugin. Specialized packages can be private to the package that provides it. But also can be made public by defining it as a package product.

There are two kinds:

An example of using a command plugin

Accessible from the right-click menu on Package.swift in Xcode. It then asks you which of the targets you wish to pass to the plugin. You can choose to invoke on the whole package. You can also pass custom arguments to the plugin.

You can also use the swift package plugin subcommand. swift package plugin --list to list the action verbs, and swift package <verb> to invoke plugin.

How do they work?

Each plugin runs as a separate process (in a sandbox that prevents network access and only has limited file system write access, e.g. build outputs directory). They have access to:

Plugins can extend sandbox, ask for permission to also modify files in the package source directory.

Plugins can emit warnings and errors.

The PackagePlugin provides the APIs.

@main
struct MyPlugin: CommandPlugin {
	// Entry points specific to package capability
}

More about command plugins

More about build plugins

To specify which build tool plugins to apply to a package target, there’s a new plugins parameter available on targets in Package.swift.

I wonder if there’s a way to preview the generated code?