Class only protocol - why the examples are not working?

I recently started modifying my app using protocol-oriented programming style (I mean, using as little class inheritance as possible) and ran into a strange issue: Swift failed to recognize that a parameter is of class only protocol type. Please see the examples below. Example 1 and 4 worked, but example 2 and 3 failed (though I really can't see why they don't work). Could it be a known issue? If so, what's the general guideline to avoid such issue? It will be greatly appreciated if anyone can help to take a look and share how you think about it. Thanks.

Example 1: This is the baseline example. It works

protocol Proto1 {
    var n: Int { get set }
}

extension Proto1 {
    mutating func modifyN(_ newValue: Int) {
        self.n = newValue
    }
}

protocol Proto2: AnyObject {
    var proto1: Proto1 { get set }
}

func test(proto2: Proto2) {
    proto2.proto1.modifyN(100)
}

Example 2: Not working

protocol Proto1 {
    var n: Int { get set }
}

extension Proto1 {
    mutating func modifyN(_ newValue: Int) {
        self.n = newValue
    }
}

protocol Proto2 {
    var proto1: Proto1 { get set }
}

func test(proto2: Proto2 & AnyObject) {
    proto2.proto1.modifyN(100)
}

// Compilation error: Cannot use mutating member on immutable  *value* : 'proto2' is a 'let' constant

Example 3: Not working

protocol Proto1 {
    var n: Int { get set }
}

extension Proto1 {
    mutating func modifyN(_ newValue: Int) {
        self.n = newValue
    }
}

protocol Proto2 {
    var proto1: Proto1 { get set }
}

protocol Proto3: AnyObject, Proto2 {

}

func test(proto3: Proto3) {
    proto3.proto1.modifyN(100)
}

// Compilation error: Cannot use mutating member on immutable  *value* : 'proto3' is a 'let' constant

Example 4: This works. The only difference between this example and the example 3 is that in this example, we let Proto2, instead of Proto3, to conform to AnyObject.

protocol Proto1 {
    var n: Int { get set }
}

extension Proto1 {
    mutating func modifyN(_ newValue: Int) {
        self.n = newValue
    }
}

protocol Proto2: AnyObject {
    var proto1: Proto1 { get set }
}

protocol Proto3: Proto2 {

}

func test(proto3: Proto3) {
    proto3.proto1.modifyN(100)
}

It seems that the rule in the current implementation is that the protocol being modified has to defined explicitly to conform to AnyObject. Otherwise it would fail to compile. That seems to be a limitation in my opinion.

1 Like

This is because mutating means that self can be reassigned, which is not permitted for let constants even of reference type (i.e., classes). Recall that when you modify a struct member, you modify the whole struct, but this is not the case when you modify a class member.

This is a common point of confusion and is discussed at length here and here and here and here.

1 Like

@xwu Thanks for the pointers! I have never read anything about this "feature" before. I'm still digesting it. But I have a quick question: if so, why example 1 and 4 work? In these two examples Proto2 conforms to AnyObject directly. How does that make the difference?

1 Like

Those examples work because the AnyObject constraint tells the compiler that proto3 is some object of reference type, which means that modifications to its members can be made without reassigning the entire value. So the line in test can be thought of as expanding to the following:

var tmp = proto3.proto1
tmp.modifyN(100)
proto3.proto1 = tmp

Because proto3 is known to be a reference type, the assignment on the last line is perfectly legal. When proto3 isn't known to be a reference type, it's illegal, because modifying proto1 might modify the value of proto3 itself.

ETA: As noted below, there's a bit more to this story. In addition to proto3 being of reference type, it's also important that the proto1 member is defined on a protocol that is AnyObject constrained.

Jumhyn, thanks for your answer. But I doubt it a bit, because proto3 in my example 3 is also a reference type (note proto3 conforms to AnyObject in example 3) but it doesn't work.

Ah, yeah, this gets back to the problem that @xwu pointed out with the reassignment of self. Consider if we updated the requirements for Proto2 and added an extension:

protocol Proto2 {
    init()
    var proto1: Proto1 { get set }
}

extension Proto2 {
    var proto1: Proto1 {
        get { fatalError() }
        set { self = Self() }
    }
}

This is perfectly legal, since Proto2 need not be a reference type—self can be freely reassigned. However, attempting to invoke this on let of type Proto3 is illegal, since self can't be reassigned in that case!

But in the case where Proto2 is constrained to AnyObject, the definition of that extension is illegal. Inserting that constraint gives us an error on the self = Self() line:

error: Cannot assign to value: 'self' is immutable

IOW, this:

is basically correct—because self may be reassigned in a mutating member of a non-AnyObject-constrained protocol, you cannot invoke such members on any lets: references or values.

Another workaround, besides making Proto2 constrained to AnyObject, is to make the setter nonmutating:

protocol Proto2 {
    var proto1: Proto1 { get nonmutating set }
}

protocol Proto3: AnyObject, Proto2 {

}

func test(proto3: Proto3) {
    proto3.proto1.modifyN(100) // OK
}

This has the same effect of making self immutable within the setter for proto1, so we know that self won't get reassigned. This will still allow value types to conform to Proto2, they just won't be able to modify any of their members in the setter for proto1.

2 Likes

Thanks. I completely agree with what you said. nonmutating seems a perfect solution to this issue. I can't find a evolution proposal for the keyword, but from the articles on the net it was in Swift as early as in 2016. That's strange, because none of the references @xwu gave mentioned it.

Looks like nonmutating was introduced in April of 2014, i.e., before Swift was even publicly announced and well before it was properly open-sourced.

1 Like

Thanks all for the discussion. For people who are still confused by the issue, below is my summary.

1) What this issue is (or: why example 2 and 3 don't work)

First some background:

  • For reference type, self is immutable. So it's impossible for an instance method to assign a new instance to self (compiler would throw an error if we did that).

  • For value type, however, that is fine. A mutating func can overwrite the entire structure.

The issue arises when we mix class and protocol which has mutating method.

  • When we define a protocol without adding a AnyObject contraint, Swift has no idea that the protocol is for class only. As a result, if the method modifies the instance's members, it has to be defined as mutating.

    Unfortunately defining a func as mutating gives it more power than we want - we may just want to let it modify instance members, but potentially it can modifiy self. So, even the actual func doesn't modify self, Swift compiler has to prepare for the worst case and assume the func modifies self.

  • That's the reason why the issue arises when we define a class and let the class conform to that protocol. The mutating method defined in the protocol can modify self! But this shouldn't occur if the instance variable is read-only. This is an unexpected issue because issue like this will never ever occur if we use class without protocol (see item 1 in background).

2) Why example 1 and 4 work

It's because we define Proto2 to conform to AnyObject. I guess that constraint makes the compiler processes the protocol's methods in a similar way as it processes class's methods (for example, throw error if a method tries to overwrite self). Since the compiler has made sure that it's impossible for the methods to overwrite self, it's fine.

3) How to work around it

  • Approach 1: make Proto2 to conform to AnyObject

  • Approach 2: using nonmutating keyword. See details in @Jumhyn's answer.

4) More thoughts

In general, what concern me is that if it's good idea to refactor a Swift project in protocol-oriented programming way (note that the project has to use some class types). It feels like that class and protocol don't play well in general and there are a lot of pitfalls. Some are solvable, some are probably not. For example, @jrose made the following comment in this thread:

Even beyond this, mutating and classes don't play so well together. It may not be the best model for what you're trying to do.

I wonder what are people's experience on using class type and protocol-oriented programming in general? Are there any best practice on this?

2 Likes

I've been following essentially one rule. If I do:

let x = X(), y = x

then mutate a member of x, e.g., x.someMember,

  • If that mutation needs to be visible from y, I use class,
  • If that mutation shouldn't be visible from y, I use struct.

Mostly because the first one implies a sense of identity.

Oddly enough, I rarely encounter mutating protocol requirements applied to class.

1 Like

Thanks for the sharing. I think what you said is: if a change to variable A should be visible/shared by variable B, then variable A should be reference type. I completely agree with that. But I find there are other factors (e.g. language constraints) affecting the decision in the practice. Below is a real example in my code where I'd like to use struct but ended up using class, because otherwise I couldn't work around the "Escaping closure captures mutating self parameter" error.

I'm refactoring a small module in my app to use state machine. The module is a struct and have a set of methods. The new design is that each method call to this struct is translated to an event to the state machine in the struct.

My first thought was to implement the state machine as a struct too. Below is the pseudo code (for simplicity I don't use generic type):

struct StateMachine {
    var currentState: State
    var transitions: [Transitions]

    // Add a transition to the state machine. 
    // onEntry param is a closure executed when transisting to the new state.
    mutating func addTransition(event: Event, from: State, to: State, onEntry: (() -> Void)? = nil) {
        ...
    }

    // Send an event to the state machine
    mutating func receive(event: Event) {
        ... 
    }
}

struct MyModule {
   var sm: StateMachine

   mutating func setupStateMachine() {
       // Note this can't compile. 
       // Error: Escaping closure captures mutating self parameter
       sm.addTransition(event: .eventA, from: .idle, to: .stateA) {
           // Do something
       }
   }

   mutating func methodA() {
       sm.receive(event: .eventA)
   }
}

Two observations:

  1. All the above methods have to be mutating. For StateMachine.addTransition(), it's because it modifies transitions array. For StateMachine.receive(), it's because it modifies currentState. For MyModule.setupStateMachine(), it's because it calls sm.addTransition() and hence modifies sm. For MyModule.methodA(), it's because it calls sm.receive() and hence modifies sm.

  2. The above code doesn't compile because the closure in MyModule.setupStatemachine() causes the "Escaping closure captures mutating self parameter" error.

The error occurs because:

  • The closure passed to sm.addTransition() has to be escaping.
  • The MyModule.sm property has to be modified by sm.addTransition() (and hence by setupStateMachine()).

I think this is quite a simple design and I doubt if there exists any reasonable design in which the above two points don't hold true. If so, that means the only possible way to resolve the compilation error is to change StateMachine to a class.

Side note: the more I deal with issue like this, the more I have the impression that, while closure really shines in functional programming, where variables are immutable, it seems to cause a lot of issues with mutable value types.

The usual solution to mutating state inside of an escaping closure is to pass that state as an inout parameter to the closure. Your transition closure should be: (inout State) -> Void, then receive should pass in state when it calls the transition.

The other solution would be to have the transition function return the new state, and have receive assign the result of calling the transition to state.

You don't need to make onEntry optional to give it a default value. Assigning a no-op closure should work fine. Although you might need to explicitly discard the argument if it has one. That can be done like so: onEntry: @escaping (inout State) -> () = { _ in }

1 Like

Thanks for your suggestions! They are very helpful and make me to reexamine my code. I have got it working to use struct (though the solution is not exactly same as you suggested. See more below).

First, sorry that the pseudocode I pasted missed a detail: onEntry don't modify state machine's state. It's receive() that does it (same as you suggested).

mutating func receive(event: Event) throws {
    // hardcode the error handler
    guard let transition = lookForTransition(event: event, from: currentState) else { preconditionFailure("unexpected event") }
    
    currentState = transition.to
    try transition.onEntry?()
}

If so, why the "Escaping closure captures mutating self parameter" error? In the past MyModule struct had mutable state (they are the application-specific state in the struct, not state in state machine) and the closures modified the state. But I had managed to remove those state already. Your suggestions reminded me to look at the closure again and I figured out why: the closure called MyModule's method to do real work and hence implicitly captured the mutating self. Once I capture self explicitly to make it immutable, the error is gone!

mutating func setupStateMachine() {
   // This work!
   sm.addTransition(event: .eventA, from: .idle, to: .stateA) { [self] in
       // Call MyModule's method to do real work.
   }

}

But what if I couldn't get rid of the mutable state in MyModule struct? Perhaps in that case I can use your first suggestion?

It never occurred to me that I can use this approach to "work around" the "Escaping closure captures mutating self parameter" error! Will certainly try it next time when I need it. Since the closure changes variables in MyModule, instead of StateMachine, to use this approach I think I need to define the signature as:

(inout MyModule) -> Void

That means the state machine needs to be aware of the MyModule struct.

Didn't know that. Thanks!

Just FYI. I find another language constraint that prevents from implementing state machine by using struct. It's the exclusive memory access feature in Swift.

When a state machine receives an event, it calls onEntry() closure.

StateMachine.receive()
-> look for a matched transition
-> update the state machine's current state
-> run the new state's onEntry() closure

If the onEntry() closure makes changes which triggers another event, there will be nested receive() calls (unless the code implements, say, an event queue mechanism to avoid that).

StateMachine.receive()
-> Transition.onEntry()
   -> StateMachine.receive()
      -> Transition.onEntry()

Since receive() is a mutating method (it updates current state), this violates exclusive memory access requirement in Swift.

So the rule is not really that simple in practice.

Terms of Service

Privacy Policy

Cookie Policy