While working with Swift's conditional conformance system, I discovered something that to me seems odd. This situation is more than a little complicated, so bear with me, and maybe someone out there can help me figure this out.
I'm working on a write-safe implementation of a Synchronized
property wrapper, based on objc.io's Atomic wrapper. My code is as follows:
import Foundation
@propertyWrapper
final class Synchronized<Value> {
private let queue: DispatchQueue
private var value: Value
init(wrappedValue: Value) {
queue = DispatchQueue(label: "...")
value = wrappedValue
}
var projectedValue: Synchronized<Value> { return self }
var wrappedValue: Value {
return queue.sync { value }
}
func modify(_ mutation: (inout Value) throws -> Void) rethrows {
return try queue.sync { try mutation(&value) }
}
}
The property is declared simply like so:
@Synchronized var myData: [String: Any] = [:]
Simply put, this type enforces that any access to its underlying value under normal conditions is synchronized with a private dispatch queue, effectively rendering the wrapped property thread-safe. I make wrappedValue
read-only, to avoid creeping data race issues with the likes of the +=
operator, or subscript methods.
Reading is a breeze, but writing to it has always been a bit of a syntactical pain. To mitigate the _underscore madness, I use projectedValue
to expose a reference to the Synchronized
instance. Per the safety requirements of my implementation, modifications to the wrapped value are gated like so:
$myData.modify { $0 = anotherDataset }
$myData.modify { $0["someKey"] = aValue }
I'm okay with this for the most part. It gets the idea across to the caller the gated nature of such operations on this object. To make simple assignment easier, I added a helper method:
func set(to newValue: Value) {
self.modify { $0 = newValue }
}
This way, I can call $myData.set(to: somethingElse)
, and it looks much nicer than modify(_:)
.
My biggest problem is that when I just want to call a convenience method on the value, such as Array
's removeAll()
method, I must wrap that!
@Synchronized var myArray = [1, 2, 3]
$myArray.modify { $0.removeAll() } // The meat is hidden behind a wall of {}
This is only one case, but there are many convenient functions I'd like to make available on my Synchronized
object if its value conforms to the appropriate protocols.
My present solution is to make Synchronized
conform conditionally to Collection
, MutableCollection
, and RangeReplaceableCollection
, implementing the relevant requirements using queue.sync
as in the base class.
Now comes the fun part.
Synchronized
conforms to RangeReplaceableCollection
just fine if, as per the documentation, I only provide an empty initializer and replaceSubrange<C>(_:with:)
.
But when I try to use the aforementioned protocol's mutating
functions, such as append(_:)
or remove(at:)
, on the instance returned with the property wrapper's projectedValue
, the compiler complains that the latter is immutable!
_myArray.removeAll() // Compiles fine 😊
$myArray.removeAll() // !! Cannot use mutating member on immutable value: '$array' is immutable
This is not the case if I implement the functions myself.
I had thought that, since Synchronized
is a class-bound type, its reference semantics are beyond the purview of most mutable
checks. In fact, modify(_:)
is in a way a mutating function, though I need not specify that in a class-bound type. The generated accessors for RangeReplaceableCollection
's methods are declared as mutating
... is that what gives the compiler a conniption?
When I implement the offending functions myself, the compiler won't let me declare them as mutating
, but happily compiles them and runs them as though nothing was ever wrong.
My question seems to boil down to this: is this a bug in Swift's compiler? Should my projectedValue remain nonmutating, and is there a way to get this to work without having to reference the _underscored property name every time I want to do a thing?