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
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
“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 opaquesome
type.
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>
.)
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.)
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.
I think this is a concept that is more simple than structured concurrency (that is, Task
s 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.
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.)
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
}}
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 likeget
, that communicate when the results of a call are not directly returned.
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.
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.)
AsyncSequence
For example, you could await center.notifications(named: …).first { … }
.
FileHandle
adds bytes: AsyncBytes
AsyncSequence
that converts an AsyncSequence
of bytes into one of linesURL
adds convenience resourceBytes: AsyncBytes
and lines: AsyncLineSequence<AsyncBytes>
async sequences.URLSession
has instance method func bytes(for: URLRequest) async throws -> (AsyncBytes, URLResponse)
NotificationCenter
adds func notifications(named: Notification.Name, object: AnyObject) -> Notifications
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.
I’m not sure what the correct language is that corresponds to concepts like “publish an element”, if I wanted to document an API.
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.
Sounds like this is the package that introduces interaction with Clock
.
Combining async sequences into a single output
Zip
Combines values into tuples. Rethrows errors. Each side is waited concurrently (i.e. one doesn’t block the other)
Merge
Combine multiple AsyncSequences into one AsyncSequence
If any of the iterations produces an error, the other iterations are cancelled.
There are some APIs for leveraging the new Clock
, Instant
and Duration
in Swift 5.7. Let’s first understand what these types are.
Clock
: Protocol for defining time. Defines a way to wake up after a given instant and a concept of now. Two of the most common ones are SuspendingClock
(stops when machine sleeps) and ContinuousClock
(always runs). We have clock.sleep(until:)
and clock.measure { … }
.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
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
Initialize Dictionary
, Set
or Array
from (known finite) async sequences
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?
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.
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.
When a task is cancelled, all subtasks that are descendents of that task will be automatically cancelled too.
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.
They fall into two categories:
Here’s a summary from the end of the talk:
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.
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.
Not all tasks fit a structured pattern:
async
contextsWe 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).
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)
.
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.
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:
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.
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
}
createBuildCommands(context: PluginContext, target: Target) throws -> [Command]
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?