Extending a class-bound type to conform to a protocol extended with `mutating` functions

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?

Rather than trying to make it conform to the protocols, offering extensions with useful APIs when the inner value conforms gets you much of the way there with far less work.

I can agree there. I'm mainly wondering whether Swift is incorrect in saying that my class-bound type's projectedValue is immutable.

Swift is correct: the difference lies in reassigning self.

I think this confusion is quite common and has to do with the way Swift tries to simplify these things (immutable and mutable value- and reference types) by "pretending" that you don't have to think in terms of pointers, although you have to anyway.

Perhaps try to look at it this way:

  • A variable or let constant of reference type has a value (just like a variable or let constant of value type has). But their value is the "address of their instance" / which instance they are pointing at / their arrow / what they are referencing. This is why they are called reference types, because their only value is what instance they are pointing at.

  • Mutable = A variable's value can be changed.

  • Immutable = A let constant's value cannot be changed.

So mutating a variable of a reference type C simply means exactly what it means for a value type:

  • You're allowed to change its value, ie you can make it point to another instance of C.

And that a let constant of a reference type C is immutable means exactly what it means for a value type:

  • You're not allowed to change its value, ie you cannot make it point to another instance of C (Note that this does not say anything about if you can or cannot change some value via that let constant, within that instance to which the let constant constantly points).

So, a reference type's value is its "arrow", and just as with value types, that (its arrow) is what is either mutable or immutable, nothing else.

And this is why it doesn't make sense for reference types to have mutating methods. The methods of a reference type cannot change a variable's value / arrow, they can only change data at the place where that arrow points (within the instance pointed to by the variable's arrow / value). The only way to mutate a variable of reference type is to change what it points to, ie assign another instance to it.


A code example:

class C {
    var v: Int
    let c: Int
    init(_ v: Int, _ c: Int) { (self.v, self.c) = (v, c) }
}

struct S {
    var v: Int
    let c: Int
    init(_ v: Int, _ c: Int) { (self.v, self.c) = (v, c) }
}

func test() {
    let lc = C(12, 34)
    lc.v += 1 // Compiles, because `lc` is *not* mutated, ie `lc` still
              // points to the same instance of `C`. (But *via* `lc` you
              // are reaching into the same instance of `C` that `lc` is
              // constantly pointing to, to mutate its `v`).

    lc = C(56, 78) // ERROR: Cannot assign to value: 'lc` is a let constant".
                   // Because the *value* of `lc` is what it points to.

    let ls = S(90, 12)
    ls.v += 1 // ERROR: "Left side of operator isn't mutable: `ls` is a let c…"
             // Because `S` is a value type, there's no indirection, and
             // mutating `ls.v` would imply mutating `ls`.

    // … Continue experimenting to adjust and verify mental model as needed …
}

And once we understand this, we also see that it isn't as simple as eg "every struct is a value type and all value types have value semantics":

public struct WhatAmI {
    public let sharedMutualState = SomeClass()
}
2 Likes

@Jens Thank you for your explanation; I think I see what you're saying. If I understand rightly, I cannot change what self, or any constant pointing to a class object, points to. In this case, I don't think I'm trying to do so, but am simply calling a method on an object that conforms to a protocol. Does that method inadvertently copy and reassign the pointer? I would think that RangeReplaceableCollection's removeAll() or append(_:) methods would the underlying data of the object, not alter the pointer itself. Though these methods are marked in the protocol extension as mutating, pointers to the mutated data shouldn't be affected unless the object being mutated is a struct. Is my reasoning flawed?

By (I think) similar reasoning as above, calling the same method on _array (the property wrapper itself) shouldn't compile either. The method is mutating.

Can your problem be reduced to and demonstrated by the following program:

final class Synchronized<Value> {

    var value: Value

    var projectedValue: Synchronized<Value> { return self }

    init(_ wrappedValue: Value) {
        self.value = wrappedValue
    }
}

extension Synchronized: Sequence where Value: Collection { }

extension Synchronized : Collection where Value: Collection {
    func index(after i: Value.Index) -> Value.Index {
        return value.index(after: i)
    }
    subscript(position: Value.Index) -> Value.Element {
        get { return value[position] }
    }
    var startIndex: Value.Index { return value.startIndex }
    var endIndex: Value.Index { return value.endIndex }
}

extension Synchronized : RangeReplaceableCollection where Value : RangeReplaceableCollection {
    func replaceSubrange<C>(_ subrange: Range<Value.Index>, with newElements: __owned C) where C : Collection, Synchronized.Element == C.Element {
        value.replaceSubrange(subrange, with: newElements)
    }
    convenience init() { fatalError() }

    // Uncommenting this will make it compile:
    // func removeAll(keepingCapacity keepCapacity: Bool = false) {
    //     value.removeAll(keepingCapacity: keepCapacity)
    // }

}

func test() {
    let s = Synchronized([1, 2, 3, 4])
    let pv = s.projectedValue
    pv.removeAll() // ERROR: Cannot use mutating member on immutable value: 'pv' is a 'let' constant
}
test()

You want to know if that error should be there and if so why?

EDIT: Sorry I posted the wrong code before, now it's what I meant it to be.

EDIT: Yes, yes that's it. Why doesn't projectedValue's result act like a pointer? As far as I know, removeAll() doesn't edit the pointer, only the pointed-to value.

1 Like

I'm not entirely sure ... here's an even more reduced demonstration of the same thing though:

protocol P {
    associatedtype Value: Numeric
    mutating func increaseByOne()
    var value: Value { get set }
}

extension P {
    mutating func increaseByOne() { value += 1 }
}

final class C : P {
    var value: Int
    init(_ value: Int) { self.value = value }
    // Workaround: Uncomment this to make the ERROR below go away:
    // func increaseByOne() { value += 1 }
}

func test() {
    let c = C(123)
    c.value += 1 // Compiles, as expected, since `c` is not mutated, ie `c` is still pointing to the same `C` instance.
    c.increaseByOne() // ERROR: Cannot use mutating member on immutable value: 'c' is a 'let' constant
}                     // But why? The mutating member is not mutating `c`, it's only mutating the instance that it points to.
test()                // Note that `c.value += 1` compiles, and note the workaround above.

@xwu, can you explain this?

1 Like

The default implementation mutating func increaseByOne can reassign self. Therefore, it cannot be used with a let binding.

That the method doesn’t actually reassign self is immaterial. What matters is the contract given by the method declaration. You would get the same error with a method mutating func changeNothing() { }.

3 Likes

Okay, so because this protocol may be used on a struct, which can reassign self as the mutating method evaluates, this assumption carries to class-bound types as well? That is to say, class-bound types should be prepared to have their self pointer reassigned when one of those methods is run?

I think it's a little odd that such an assumption would be permitted in Swift, seeing as nowhere anywhere does a class-bound type reassign itself... does it? (What would that even mean? Every pointer to it now points somewhere else? How does that work?)

I can kind of agree. I wrote a separate post asking about this. (It is possible to reassign self in a class, but not straight forwardly.)

1 Like

@Jens If we alter your code to instead run the self-altering default implementation of one of the protocol methods to instead run on s directly, we get an error. I wonder if it is somehow related.

func test() {
    var s = Synchronized([1, 2, 3, 4]) // var, so it'll compile
    s.removeAll() // the `mutating` default implementation
}
test() // ERROR: Execution was interrupted, reason: EXC_BAD_INSTRUCTION

This is not the case when the implementation of removeAll() is overridden in our own conformance extension to just call the same on the underlying value.

Nah, that's just this:

    convenience init() { fatalError() }

(I didn't bother to implement it.)

1 Like

Oh. So it is. I’m not used to how fatalError() looks without a message.

Terms of Service

Privacy Policy

Cookie Policy