Actors with less boilerplate code

Hello everyone.

We are currently working on refactoring for Swift 6. As part of the new changes, we have introduced actors to manage the properties of classes that were previously mutable. Here's an example implementation:

actor PropertyManager
{
        private var propertyOne: String = ""
        private var propertyTwo: String = ""
        
        func getCurrentPropertyOne() -> String
        {
            return self.propertyOne
        }
        
        func setCurrentPropertyOne(_ newValue: String)
        {
            self.propertyOne = newValue
        }
        
        func getCurrentPropertyTwo() -> String
        {
            return self.propertyTwo
        }
        
        func setCurrentPropertyTwo(_ newValue: String)
        {
            self.propertyTwo = newValue
        }
}

This manager can be used in a class to modify its values in a thread-safe way.

However, the problem with this approach is that it generates a lot of boilerplate code due to the need for explicit getters and setters.

A much more efficient solution would be to implement the manager using computed properties. However, when trying to implement a setter for a computed property, the following error occurs:

var propertyOneAccess: String
{
            get
            {
                return propertyOne
            }
            set
            {
                propertyOne = newValue
            }
}

Actor-isolated property 'photoEvaluationDoneBefore' can not be mutated from a Sendable closure

This brings us to the main question:

  1. Is there a more elegant way to implement this with less boilerplate code?
  2. Why do we need to implement all the getters and setters in an actor manually?
1 Like

Hi Tim,
I would suggest taking a step back and looking at the big picture before we discuss the specific properties question.

If all your actor is doing is just protecting a mutable variable like this, you probably don't need it to be an actor. This is coming me who's worked a decade on actor runtimes and absolutely loves the model.

The general guidance here is that actors tie operations and state together in one "place". If your operations are some complex business logic, modifying a bunch of state, and that state should never be accessed by anyone else -- that's a great case for an actor. Think like OOP's encapsulation ideas.

Now if the state is just "a bunch of state", you probably are accessing that from various places, and the async calls there won't make it nice to use, nor are you really getting much out of the actor in the first place. You're not really centralizing "working on the data" if all the actor does is just return and store it. You can use a Mutex or other synchrization primitive to store such data efficiently. So in a way, this being "annoying" with an actor, has set you on the right path to reconsider if you're doing things right here :slight_smile:

So I'd like to ask what the actor really is modeling, and representing. Because if it really is just a bag for properties, you're right, it's not going to be the most convenient. And we could discuss the properties question (why setters cannot be async), but it won't really change the outcome much -- perhaps you should look at another synchronization mechanism for this specific situation.

Actors are a great go-to for safety and getting concurrency safety, but they can be overkill sometimes as well.

8 Likes

Thank you for the detailed response!

In our case, we've decided to use this solution, as it would be more effort to refactor the entire class as an actor.

However, if you have an additional solution that could work with our current approach, we'd be happy to hear it!


Probably this a bit simpler:

actor PropertyManager {
  private(set) var propertyOne: String = ""
  func setCurrentPropertyOne(_ newValue: String) {
    self.propertyOne = newValue
  }
}

let manager = PropertyManager()
await manager.setCurrentPropertyOne("Hello")
print(await manager.propertyOne)

Also probably regular var propertyOne: String = "" just fine, cause compiler already fires an error you can't mutate it.

Would be nice to have a bit more info on architecture or approach you're using, could be it's ok with actors.