How can I conform a Swift actor to a protocol while preserving isolation?
Asked Answered
C

3

17

Let's say we have the following goals:

  1. We want to use actors.
  2. We want to use dependency injection and resolve actors through DI.
  3. We want to hide our actors behind protocols so we can vary the implementation.
  4. We want to preserve actor isolation, otherwise there's no point in using actors.

So the specific question is: How can we conform a Swift actor to a protocol while preserving isolation?

protocol Zippity {
  func foo()
}

actor Zappity: Zippity {
  func foo() {} // Doesn't compile
}

I can make it compile with …

actor Zappity: Zippity {
  nonisolated func foo() {}
}

… but that seems to defeat the purpose. I also tried declaring the interface method async but that also didn't compile.

I can think of several reasonable workarounds, including composition, nonisolated async methods that call isolated methods, etc. but wanted to see if there's something I'm missing.

Cavernous answered 7/1, 2022 at 1:0 Comment(0)
C
37

OK, I've discovered the answer, and it's pretty straightforward:

protocol Zippity: Actor {
  func foo()
}

actor Zappity: Zippity {
  func foo() // This is a properly isolated method
}

It seems that declaring conformity to the Actor protocol enables the compiler magic.

Cavernous answered 7/1, 2022 at 14:44 Comment(3)
Btw, do you know any way of sharing that protocol with a class without implementing the Actor protocol on that class? Because if I also need a non-actor class (for my legacy code) on the same codebase, I'll receive an error like: Type 'Zeppity' does not conform to protocol 'Actor'.Kendre
@RicardoBarroso unfortunately, for an actor to conform to a protocol without adding the nonisolated keyword, which would ultimately kill the idea of an actor, is to add the async keyword to the function, if your legacy code is in Swift it’ll probably work. But if it’s in obj-c you’ll find some trouble. I’d recommend either building two different protocols, or using a wrapper to objc with Tasks.Pearly
Thanks @PedroFarina. Meanwhile, I solved it exactly as one of your recommendations, having 2 different protocols.Kendre
K
1

As long as the protocol does not use property setters, marking every method and property getter as async will let an actor conform to the protocol. As it is not possible to have async setters, conformance to Actor is needed in that case.

So in your example:

protocol Zippity {
    func foo() async
}
Keener answered 18/11, 2022 at 10:6 Comment(0)
P
1

Actors are only able to be thread safe by making all their functions implicitly async. It does some magic behind the curtains, to make sure it’s being called in the right thread.

To fix this you can either inherit from Actor on the protocol

protocol Foo: Actor {
func bar()
}

Or mark the functions async

protocol Foo {
func bar() async
}
Pearly answered 25/5, 2023 at 19:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.