Cascading `mutating` for protocols restricted to reference types

I'm happy that protocols can be restricted to specific types these days (e.g. where Self: UIViewController :+1:), but I wish that, when using that restriction on a reference type, we also achieved the cascading of an invisible mutating like I'm showing below. I'm here to gauge whether anyone else cares, and if there's agreement that it should cascade. I'll file a radar if you'd like the feature also.

import UIKit

// 1. Just inheriting from UIViewController does not compile.
// 2. `where Self: UIViewController` compiles, but doesn't cascade `mutating` like `: AnyObject` does
// 3. Adding `where Self: UIViewController` after `AnyObject` here will result in a warning about redundancy.
public protocol SomeSortOfSpecialViewController: AnyObject {
  var mutableProperty: Any {get set}
}

public extension SomeSortOfSpecialViewController where Self: UIViewController {
  // `func`s can be defined here without `mutating`s due to `: AnyObject` above.
  
  func mutateProperty() {
    mutableProperty = ""
  }
}

This pattern above doesn't carry as much information as a restriction at the protocol level, instead of the extension level, so using explicit mutatings is probably the better way to go unless we can get what I'm talking about.

1 Like

I’m a bit unclear on what you want to achieve with the protocol restrictions. As far as I know, the mutating keyword used as modifier on Swift functions is about restricting their side-effects on value types. It relates to use the of let and var keywords. This is a bit different for reference types, i.e. classes.

Class methods in Swift all take implicit self parameter and will be usually modifying the state associated with it (working with class members).
Edit: see below Which means that technically all class methods are mutating, but this is implicit, you don’t have to re-declare that for each method — that would be a lot of boilerplate.

So I’m guessing your aim with the protocol was something else, I just don’t know what…

This is incorrect. Methods of classes are not implicitly mutating and in fact are not allowed to be mutating at all. This is because mutating is how we spell inout self and in a method of a class self is an object reference. This means that a mutating method on a class would allow reassignment of the object reference. There are edge cases where this behavior can be observed. Here is a demonstration:

class Foo {
    deinit {
        print("deinit \(ObjectIdentifier(self))")
    }
}

let f1 = Foo()
var f2 = Foo()

protocol MakeF1 {
    mutating func makeF1()
}
extension MakeF1 {
    mutating func makeF1() {
        self = f1 as! Self
    }
}

extension Foo: MakeF1 {}


print("f1 \(ObjectIdentifier(f1))")
print("f2 \(ObjectIdentifier(f2))")
print(f1 === f2) // false

f2.makeF1()

print("f2 \(ObjectIdentifier(f2))")
print(f1 === f2) // true

The instance initially assigned to f2 is deinitialized and the object reference is reassigned such that both references now point to the same instance.

Note that the protocol extension is necessary to observe this behavior. You cannot implement a mutating method directly on the class itself. The compiler does allow methods that do not include the mutating modifier to satisfy mutating protocol requirements. This is probably what you were referring to when talking class methods being implicitly mutating.

3 Likes

Yes! Thank you for clearing up my misconception! Could you also address the OP?

The warning produced by protocol SomeSortOfSpecialViewController: AnyObject where Self: UIViewController looks ok to me as it should be ok to just say protocol SomeSortOfSpecialViewController where Self: UIViewController. The fact that the latter produces "self is immutable" errors when setters are used looks like a bug to me. The compiler should know that UIViewController is a class and thus setters are available even when self is immutable. If there isn't already a ticket for this I recommend @anon9791410 file one.

1 Like

Thanks! I slept on it and remembered that I filed it in October as 34812895. I'll add this thread for more detail.