Replace threadsafe var with new concurrency model (Actor?)

I doubt it for this app…

The interactions are particularly complex and, from what I have seen, a lot of logic code in SwiftUI gets subsumed into View classes - something I detest. And so far, it has taken nearly 3 years to get to its current state, so SwiftUI was very immature and I'm not even thinking about rewriting it.

I only see standard controls on this screenshot so the UI itself looks a 3 week job implemented in SwiftUI (with perhaps 70% implemented in 3 hours and 90% implemented in 3 days). Here I am talking of UI only plus a mock data model to drive it. For AppKit implementation I'd have to multiply those estimates by 3. We can take that part of conversation offline as we've significantly deviated from the topic. :wink:

PS. I appreciate the final percent can take an unbound amount of time no matter what technology is used.

PPS. Enbolded the relevant points to not reiterate them.

Wow! So you know how to write a basic UI, but do you know how to:

  • read and write metadata to image files or XMP sidecars
  • create dynamic lookups for an NSTokenField
  • create popup menus for the thumbnails
  • create metadata search queries
  • integrate with macOS Finder tags
  • implement drag and drop of image files that contain keywords
  • implement Quicklook preview panels
  • implement a search UI that allows both "starts with" and "contains" logic, that also allows for ANDing and ORing of predicates?
  • implement a star rating search tool that allows multiple ratings
  • etc, etc.

What you see in the screenshot might appear simple but, speaking as a developer with 30 years experience in DOS, Windows, Mac and iOS development, I would call your assertions about the alleged simplicity of SwiftUI a wee bit presumptuous.

It is rarely what you see that takes the time, it is what you don't see. Something I have taught many developers in my role as a software analysis and design consultant.

2 Likes

This is not any less true with any other synchronization patterns under concurrency. Fine-grained synchronous locking and Joanna's ThreadsafeVar have exactly the same interleaving and ordering problems you're identifying with actors. Getting programmers to think carefully about atomicity/transactionality is just an inherent problem in concurrent programming.

1 Like

The point I was trying to make is that code that is isolated to the context of a thread/queue/actor is safer when it does not call async functions because it cannot be the victim of interleaved code execution. Therefore creating a basic async primitive such as this one would be a bad idea (in my opinion) because it would invite async calls in many places way too easily.

Except atomicity is much simpler to guarantee when you can write using synchronous constructs rather than requiring async everywhere, for the reasons @tclementdev outlines. Swift's concurrency features don't allow for such constructs and there's no way to mark existing constructs (locks, etc.) in such a way as the compiler can see them as safe.

You can have a function that calls a synchronous function while holding a lock.

Yes... that's what makes it easier to reason about. I can guarantee I hold exclusive access to a resource until all of my work is done. With concurrency you can only make a similar guarantee if you stay synchronous within the awaited work. A single need to await means you lose your atomicity or, at the least, make it much harder to reason about. Plus you require every caller to adopt async syntax in the first place, making it rather viral.

1 Like

I’m saying that making things synchronous in order to prevent suspension and interleaving is a tool provided by the language which you can use in order to get the effect you want. You don’t have to make actor functions async; they’ll be observed as async from non-isolated code, but internally they won’t allow suspension.

1 Like

This is the best I have come up with so far in terms of wrapping thread safe actors in external access properties. Still need a separation of read and write state... At least I can now confirm that any global state access writes will be thread safe.

NOTE: The idea is completely unworkable for anything by simple value types. Especially if you have a struct that is altered by multiple code paths. Taking a copy of the state and then writing it will mean you cannot guarantee any persistence.

// A use case requires it to have access to a global state controller...
protocol UseCase: AnyObject, CustomStringConvertible {
    var state: GlobalState { get async }
    var mutableState: GlobalState { get async }
    var stateController: GlobalStateController { get }
    var queue: DispatchQueue { get }
}

extension UseCase {
    var description: String {
        "\(String(describing: type(of: self))): This use case does not have a description..."
    }

    var state: GlobalState {
        get async {
            await stateController.readState()
        }
    }

    var mutableState: GlobalState {
        get {
            fatalError("Mutable state has no read component")
        }

        set {
            Task {
                await stateController.writeState(newValue)
            }
        }
    }
}

actor GlobalStateController {
 ... lots of funky read/write stuff here 
}

struct GlobalState: Storable { // Storable is Equatable and Hashable
 ... global state goes here 
}