Video #355: Beyond Basics: Isolation, ~Copyable, ~Escapable
Episode: Video #355 Date: Feb 23, 2026 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep355-beyond-basics-isolation-copyable-escapable

Description
It’s time to go beyond the basics with a deep exploration of isolation, noncopyable, and nonescapable types. But before we get into all the nitty gritty details we will demonstrate why understanding these topics matters, starting with a preview of isolation in Composable Architecture 2.0.
Video
Cloudflare Stream video ID: 77912d3083f35ad6190a081359782789 Local file: video_355_beyond-basics-isolation-copyable-escapable.mp4 *(download with --video 355)*
References
- Discussions
- Combine Schedulers
- Clocks
- combine-schedulers
- 0355-beyond-basics-isolation-pt1
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We are incredibly excited to embark on one of our most ambitious series ever on Point-Free. We are going beyond the basics by cover 3 major topics in the Swift programming language that allow you to express powerful invariants in your code. When used properly you can construct APIs for you and your team to use so that the Swift compiler naturally guides you to the correct usage. Things that should not be possible will be provably impossible, enforced by the compiler.
— 0:35
These three topics are as follows:
— 0:37
Isolation : By far the most important concept in Swift concurrency is that of “isolation”. It is a static, compiler enforced guarantee that a particular region of your code will never be simultaneously accessed from multiple threads. There are a lot of misconceptions in the Swift community about what isolation is and just how viral it can be here, and we are here to bust those myths. We will show that one can embrace isolation without literally making everything in your code async and having to sprinkle awaits everywhere. Stephen
— 1:09
Next we will talk about non-copyable types. These are types with very strict ownership rules that prevent making unnecessary copies of data and restrict how the data can be passed around to functions. One of the super powers of non-copyable types is their ability for you to pass values around without ever incurring the cost of making copies. Brandon
— 1:30
And finally we will talk about non-escapable types. This is a very new tool in Swift, and is still in active development, but the fundamentals are in place and ready for use. Non-escapable types allow you to be strict with how long values can live and tie their lifetimes to the lifetimes of other values. This goes hand-in-hand with non-copyable types for even more power, and in a (hopefully) near future it will even allow for storing non -escaping closures in types, which would allow us to build powerful abstractions that do not incur the cost of thunks, heap allocation, and deep call stacks.
— 2:10
These three tools, when used together, allow you to highly restrict how your APIs can be used so that you can provide the safest possible tools, and amazingly the tools can even be more performant! Stephen
— 2:23
So, we have a lot to cover, but before getting into the nitty-gritty of these advanced Swift topics, we are going to have a little fun by giving everyone a sneak peek at some of the amazing things we have been able to build with these tools.
— 2:35
We will first show how we use these tools to build the next generation of the Composable Architecture, and in particular the Store concept. We have been able to strongly control the isolation that features execute in, which gave us the ability to support main actor stores and background stores at once (with no code duplication), and provide stronger guarantees on how effects execute, which allows tests to run deterministically without sprinkling Task.yield s all over the place. And this will all be accomplished while minimizing how much we need to employ sendable types and closures, making it very easy to use, and without using locks to enforce correctness, greatly improving performance. Brandon
— 3:13
And then we will show how we can use these tools to build a Swift wrapper around a popular C library that enforces the requirements of that C library statically. Anytime you deal with a C library you are naturally going to have to deal with pointers, which are notoriously difficult objects to correctly handle. Add on top of that this particular C library has additional restrictions on when it is and is not OK to call its methods from multiple threads, and you have a veritable minefield ahead of you that is easy to get wrong. But we will show how one can enforce correctness at compile time, and again, all without decorating every type under the sun with Sendable and using locks all over the place.
— 3:56
I hope all of this sounds exciting because it’s hard to put into words just how amazing it all is. The only way to really see it is to show it. So let’s begin. Isolation in Composable Architecture 2
— 4:08
I am in a brand new SPM package right now that represents the current state of Composable Architecture 2. It’s still got more work to do before we are ready to release it, but we are getting close to the point of bringing all of this work into the main TCA repo, piece-by-piece.
— 4:28
I am going to explore all of the amazing super powers endowed to TCA2 features thanks to a deep integration of isolation into all of its core tools. To do this I am going to write some tests to demonstrate new behavior in stores and test stores that were previously impossible. So let’s start with the scaffolding of a feature in a new test file: import TCA26 import Testing @Feature fileprivate struct EffectModificationSynchrony { struct State { var count = 0 } enum Action { case tap } var body: some Feature<State, Action> { Update { state, _ in return .run { store in // Async context for effects } } } }
— 4:51
For the uninitiated, this here is a Composable Architecture feature that allows one to model the state of the feature as a value type, describe the possible user actions for interacting with the feature, and then provides an update function that can evolve the current state to the next state when an action is received, as well as optionally kick off an effect for performing async work and feeding data back into the feature.
— 5:23
This feature has a count, a tap action, and we have stubbed an update to process the state and return an effect. Of course some of the most obviously different things in this code is that we have renamed some things: the macro is now @Feature and the thing that evolves state over time is Update . But also, interestingly, the effect takes a store , not just a send .
— 5:43
In TCA2 effects are handed store-like objects that can be sent actions, just like how it works in 1.0 today, but it has a few more super powers. One of those is that you can perform a modification to the feature’s state directly in the effect: return .run { store in try store.modify { $0.count += 1 } }
— 6:09
This compiles, and one of the more surprising things about this compiling is that it did not require an await . We are allowed to perform this work synchronously even though we are an async context, which is quite different from TCA 1.0 which requires an await to send an action. And the reason this is working is because isolation is guaranteed to be the same through every layer of the feature, starting the store, to the Update and all the way down to the effect. And because it’s synchronous we are even allowed to do fun things like surround it in withAnimation : try withAnimation { try store.modify { $0.count += 1 } } …or other continuation-based APIs.
— 7:22
Another surprising thing about this code is that we are even allowing it to be written at all. TCA has a few core tenets it prides itself on, one being that all changes to state happen through an update function, and this seems to go against the grain. However, it does not. We are enlarging what it means for all state changes to happen in the update function by including the body of the effect too. And this is further enforced by the fact that we have locked down how the store argument can be used that is handed to effects. In particular, these values are now non-copyable, which means they are highly restricted in how they can be used and cannot be escaped to other contexts.
— 8:03
And the reason we are able to do this without fear of injecting uncertainty into our features is because it’s still 100% testable. Because all of the layers share the same isolation, we can guarantee that the effect starts synchronously and immediately, and we are able to assert on this behavior deterministically.
— 8:41
I am first going to explore this from the perspective of just a regular store. I will define a test directly in the feature, because we can treat the feature itself as a Swift Testing test case: @MainActor @Test func effect modifications are visible synchronously() { }
— 9:07
We will start by creating a store: let store = Store(initialState: State()) { self }
— 9:34
Ideally we would be able to send the .tap action and then immediately see the result of the modification: store.send(.tap) #expect(store.count == 1) This may seem impossible since we aren’t even performing any await s here! How could we hope that the action is processed by the store, an effect starts up, and runs its synchronous behavior, all before the send method completes.
— 10:20
Well, amazingly, that is exactly what happens. This test passes, even if we run it 1,000 times.
— 10:41
And the same goes for a test store too. We can copy and paste the test and make a few changes, mainly swapping in a TestStore and using the test store’s powerful assertion tools, where we get to exhaustively prove how state changes: @MainActor @Test func test store: effect modifications are visible synchronously() { let store = TestStore(initialState: State()) { self } store.send(\.tap) { $0.count = 1 } }
— 11:30
And this too, passes, and it does so 1,000 times, 100% of the time.
— 11:39
But even cooler, because we have so fully controlled isolation, we get to introduce a brand new tool: TestStoreActor . We can swap one in by adding a few await s and we even get to drop @MainActor from the test: @Test func test store actor: effect modifications are visible synchronously() async { let store = await TestStoreActor(initialState: State()) { self } await store.send(\.tap) { $0.count = 1 } }
— 11:50
It is now possible to run your features in a test store that is not bound to the main actor, but rather to its own actor. This means each test will run in a separate actor giving us much better parallelization during tests. This has actual real world benefits. Large test suites that primarily deal with main actor bound objects, such as TCA1 stores or @Observable models in vanilla Swift, become slower over time due to the amount of contention on the main thread. And you always have the option to use a main actor test store if you want, but there’s really no reason to unless you are building your features in a target with default main actor isolation, which by the way is fully supported in TCA2.
— 13:04
OK, this is cool and all, but it gets so, so much better. Right now performing the modification in the effect is a little silly because it could have just as easily been done in the update instead: state.count += 1 return .none
— 13:26
But suppose we wanted to perform this mutation after consulting with a dependency that has an async status() endpoint: guard await client.status() else { return } try store.modify { $0.count += 1 }
— 13:56
This is the kind of code that destroys testability. We have no idea how long client.status() is going to suspend for, and so we have no choice but to sprinkle in yields or sleeps to force things along and hope we can assert on how state changed after. TCA 1.0 tries to help with this by sending explicit actions instead of making direct modifications: return .run { send in await send(.client(client.status())) }
— 14:37
…and that lets the test store receive the response to make assertions. But that also leads to a lot of ping-ponging of actions, which is something a lot of people complain about in TCA.
— 15:02
So, would you be amazed to learn that this problem is now completely solvable with no yields or sleeps? To see this, let’s design this little client that has a single async endpoint for checking the status of some external system: private struct Client { var status: () async -> Bool }
— 15:28
But we are going to make one crucial change to this type: private struct Client { var status: nonisolated(nonsending) () async -> Bool }
— 15:33
We are using nonisolated(nonsending) to allow the isolation propagate into the implementation of this endpoint from the caller. We will have a lot to say about nonisolated(nonsending) later in this series.
— 15:46
This now allows us to write live clients that are actually capable of performing async work: static var live: Self { Self { await Task.yield() return true } }
— 16:01
But mock clients do not perform any async work: static func mock(status: Bool = true) -> Self { Self { status } }
— 16:13
And thanks to nonisolated(nonsending) Swift will further squash the suspension point so that we do not get a thread hop, and our effect stays nice and synchronous.
— 16:26
To prove this, let’s add a client to our feature: var client: Client?
— 16:31
I am going to make it optional so that it’s easy for tests to opt out of this who are not interested in it, and I’m not using our dependencies machinery just to keep things simple. And I will update the effect to unwrap the client and check its status: if let client { guard await client.status() else { return } } try store.modify { $0.count += 1 } Another benefit of making the client optional is that tests that do not provide a client will not incur a suspension at all, and so all tests still pass deterministically.
— 16:51
But now we can write a test that does inject a client, and we will start with seeing what’s wrong with using a live dependency in the test: @MainActor @Test mutating func effect modification _not_ visible after real suspension() async throws { client = .live let store = await TestStoreActor(initialState: State()) { self } await store.send(\.tap) { $0.count = 1 } }
— 17:39
After tapping we would hope we could immediately see the count go up to 1.
— 18:16
But this fails: Issue recorded: State changes expected, but none occurred: … And this is because we hit the Task.yield in the dependency, and that ever so slightly delays the execution of the store modification.
— 18:27
If we ever so slightly delay our assertion we seem to be able to assert: await store.send(\.tap) await Task.yield() await store.expect { $0.count = 1 }
— 18:44
And the test passes. But that’s just luck. If we run the test 1,000 times it fails a significant percentage of the time: 278/1000 (28%) failed
— 19:03
So we unfortunately have no choice but to sleep for a small amount of time: try await Task.sleep(for: .seconds(0.1))
— 19:18
…and now tests seem to pass, even when run 1,000 times, but I of course have no confidence in this test and these sleeps are only going to slow down our test suite. It took almost 2 minutes to repeatedly run this one test. And I even have less confidence that this test will continue to pass as the suite grows and more and more parallel contention is happening. Nor do I have confidence that this test will pass in resource-constrained environments like CI.
— 20:07
Funnily enough this kind of test is easier to write in TCA1 because the explicit store.receive gives us an opportunity to wait a bit of time for the action to come in. But it’s of course at the cost of the test store having lots of yields and sleeps sprinkled inside that really should not be there.
— 20:36
And so now let’s see the amazing part. We are going to show that once you use a mock in these tests, all of the suspension points and non-determinism go away. We will write a new test that uses a mock with status already set to true : client = .mock(status: true)
— 21:10
This test passes immediately, even if we run it 1,000 times. And it passes in a paltry second or so, compared to a moment ago where we waited nearly two minutes for tests to run. And we are accomplishing all of this without a single Task.yield in the core of the TCA2 library. Synchronous sleep Brandon
— 21:46
So it’s pretty incredible to see what Swift’s isolation tools bring to the Composable Architecture 2.0. By meticulously designing the library with isolation in mind we are able to provide all new tools that are 100% safe, our features can use everything Swift concurrency has to offer, and all the while we can still write completely deterministic tests. And I think a lot of people who tuned into our live stream and got a quick glimpse of these tools were a little afraid that we were opening Pandora’s box when it came to testability, but I hope this has alleviated their fears. Exhaustive testing is still 100% possible. Stephen
— 22:22
Once we had a handle on how to best use isolation throughout the Composable Architecture all types of new benefits started popping up left and right. One of the more amazing things is that we are now in a position to better control how time flows in our features. As many of you know, we have long maintained libraries that help with testing time, such as our Combine Schedulers library and Clocks library. Each come with a test friendly way of scheduling events in the feature, such as the ImmediateClock and TestClock , and these are tools we even built in our last series of episodes on Swift concurrency.
— 22:53
But, as many of you probably also know, those tools were not as perfect as they should be. We were forced to insert little yields into various parts in order to workaround the fact that it’s nearly impossible to reliably test async code in Swift. Well, that was up until recently. By making use of nearly every advanced tool in the box we were finally able to get 100% deterministic testing when it comes to timing.
— 23:17
Let’s take a look.
— 23:20
I am going to get the scaffolding of another test feature in place: @Feature struct SynchronousTime { struct State { var count = 0 } enum Action { case tap } var clock: any Clock<Duration> var body: some Feature<State, Action> { Update { state, _ in return .run { store in } } } }
— 23:24
Again just an integer for state, a single tap action, but this time I have gone ahead and added a clock to the feature since we will be doing something time-based in here.
— 23:34
And I am going to do this silliest thing by just incrementing the count right away, then wait a second, and increment it again: state.count += 1 return .run { store in try await clock.sleep(for: .seconds(1)) try store.modify { $0.count += 1 } }
— 23:55
This kind of code can wreak havoc on a test suite because we seem to have no choice but to wait for a full second to test this behavior. To see this let’s define a test for the default, continuous clock, where we simply construct a test store, send a tap, assert the count went up by 1, wait for a second to pass, and then assert that the count when up to 2:: var clock: any Clock<Duration> = .continuous … @Test func continuous clock() async throws { let store = await TestStoreActor(initialState: State()) { self } await store.send(\.tap) { $0.count = 1 } try await Task.sleep(for: .seconds(1)) await store.expect { $0.count = 2 } }
— 24:54
This test passes. But can we trust it? Are we sure that 100% of the time the clock.sleep in the effect will finish before the Task.sleep in the test? Well, if I run it repeatedly we eventually get a failure: Issue recorded: State changes expected, but none occurred: … − SynchronousTimeTests.State.DebugSnapshot(count: 2) + SynchronousTimeTests.State.DebugSnapshot(count: 1) (Expected: −, Actual: +)
— 25:11
And not only did we get a failure, but it took a long time to see this failure because each run of the test took a full second. What if we needed to run this test 1,000 times to get some confidence on its determinism? Are we going to wait around for 16 minutes to see that?
— 25:22
And that’s why in tests we like to use something other than a live continuous clock. A safe default to use most of the time for clocks is the ImmediateClock . When you ask it to sleep for an amount of time, it just ignores you and doesn’t sleep at all. That should allow the effect to breeze right past the clock.sleep and start executing the rest of the effect, which means we should have a better chance at testing it.
— 25:41
So, let’s copy-and-paste our previous test and start using an immediate clock instead of a live one, which also means we can stop doing Task.sleep in the test: @Test mutating func immediate clock() async throws { clock = .immediate let store = await TestStoreActor(initialState: State()) { self } await store.send(\.tap) { $0.count = 1 } await store.expect { $0.count = 2 } }
— 26:02
This test passes and now runs super fast (0.022 seconds), but still we can’t trust it. If we run it 1,000 times we will see it actually fails a decent number of times: 175/999 (18%) failed
— 26:16
This is pretty bad performance for an immediate clock, but it’s worth mentioning that this problem is particularly exacerbated by TCA2 and it actually works better in TCA1. That is because in TCA1 you would write this test by receiving an action, and that gives the system a bit more time to process things before asserting. And we help the system along by insert copious amounts of yields throughout our libraries. Clocks has 6 and TCA1 has 11!
— 26:42
But now in TCA2, with isolation controlled so tightly, we get to greatly improve this. We are working on the next generation of our clocks library that will provide a new protocol that is fully backwards compatible with the Clock protocol and fixes all of these problems”
— 27:03
It is called a nonsending clock: var clock: any NonsendingClock<Duration> = .immediate
— 27:06
…and just as nonisolated(nonsending) helped our mock dependency not suspend when not necessary, this does the same for clocks.
— 27:14
When we run the test, strangely it fails: @Test mutating func immediate clock() async throws { clock = .immediate let store = await TestStoreActor(initialState: State()) { self } await store.send(\.tap) { $0.count = 1 } await store.expect { $0.count = 2 } } Issue recorded: State change did not match expectation: … − SynchronousTimeTests.State.DebugSnapshot(count: 1) + SynchronousTimeTests.State.DebugSnapshot(count: 2) (Expected: −, Actual: +) Issue recorded: State changes expected, but none occurred. …
— 27:19
It turns out that we are already able to see the store.modify within the scope of just sending the first action. That is because the immediate clock did not even incur a thread hop from its suspension. Swift was able to breeze right past it while starting up the effect and get us to the modify immediately.
— 27:27
That means our test is now even simpler: @Test func immediate clock() async throws { let store = await testStore(clock: .immediate) await store.send(\.tap) { $0.count = 2 } }
— 27:53
We can confidently say that if we simplify the passage of time by just saying that all of time squashes into a single moment, then indeed when the user taps, the count immediately goes up to 2. Even though technically one mutation happened in the synchronous part of the Update and the other happened in the effect.
— 28:13
I can run this 1,000 times and it passes every time, and super fast. I can even run it 10,000 times and it still passes. This test is now proven to pass, deterministically, 100% of the time. This is something we were never able to accomplish in TCA1.
— 28:38
And we have accomplished by getting rid of all yields in this code and the library code it calls out to. There isn’t a single yield in TCA2 or the ImmediateClock , and that also means that large test suites are going to get a lot faster. Those yields were problematic for lots of tests running concurrently because it creates a lot of busy work for the scheduler. If you’ve got hundreds of tests running and each one is constantly nagging the scheduler to tell it to process other people’s suspending work, then you can overwhelm the scheduler and slow things down. Next time: Safe interacting with C
— 29:06
We have now shown that once isolation is strictly controlled that even time-based asynchrony can be tested in a synchronous fashion with 100% deterministic results.
— 29:15
And we only showed this for immediate clocks, but a similar thing can be shown with test clocks, which are a kind of clock that suspend forever when they sleep and only un-suspend when someone from the outside tells them that some amount of time has passed. Test clocks are great for when you want to test truly test every aspect of how time flows through your features, and helps you wiggle into every little nook and cranny of your code, but there is just a tiny bit of work that needs to be done in Swift before this is possible. We need nonisolated(nonsending) versions of both withUnsafeContinuation and withTaskCancellationHandler . Once that is possible test clocks will be able to squash all unnecessary suspension points and give us the ability to fully test the flow of time without sprinkling yields all throughout our code. Brandon
— 30:00
Let’s move on to the next demo to show off just how amazing isolation, non-copyable types and non-escaping types are. While working on this series we worked on a little demo to get a better understanding of how everything works. That demo provides a safe interface to a popular C library that enforces the rules of the library statically so that the compiler can help prevent us from interacting with the library incorrectly.
— 30:28
The C library in question here is indeed SQLite, something we have talked about a ton on recent episodes, and some might say too much. We promise we are not opening that topic back up right now. What we are discussing here really has nothing to do with SQLite and has everything to do with interacting with global mutable state in a safe, ergonomic and performant manner.
— 30:51
We are not going to be spending a ton of time explaining how SQLite works. We’ve already done that in past episodes. Instead we are going to be using it as a really interesting example to show how in shockingly few lines of code we can enforce correctness when it comes to interacting with a tricky and easy-to-get-wrong C interface.
— 31:10
I will now demo what we were able to accomplish with Swift in just a few days of playing around with Swift’s tools, and then later in this series we will show what it takes to build these tools…next time! References combine-schedulers Brandon Williams & Stephen Celis • Jun 14, 2020 An open source library that provides schedulers for making Combine more testable and more versatile. http://github.com/pointfreeco/combine-schedulers Clocks Brandon Williams & Stephen Celis • Jun 29, 2022 An open source library of ours. A few clocks that make working with Swift concurrency more testable and more versatile. https://github.com/pointfreeco/swift-clocks Downloads Sample code 0355-beyond-basics-isolation-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .