What are use cases for protocols overriding mutating/nonmutating with their opposite?

You can do it either way:

protocol Mutating {
  var property: Void { mutating get }
}

protocol Nonmutating: Mutating {
  var property: Void { nonmutating get }
}
protocol Nonmutating {
  var property: Void { nonmutating get }
}

protocol Mutating: Nonmutating {
  var property: Void { mutating get }
}

An adopter of either derived protocol is only going to need to define the nonmutating variant, making the mutating in the chain seem like a lie to me—the best you can do is shadow for overloads based on variability or different levels of the hierarchy, e.g.

protocol Nonmutating {
  nonmutating func ƒ() -> String
}

extension Nonmutating {
  nonmutating func ƒ() -> String { "nonmutating" }
}

struct Conformer: Nonmutating {
  mutating func ƒ() -> String { "mutating" }
}

Conformer().ƒ() // "nonmutating"
var variable = Conformer()
variable.ƒ() // "mutating"
(variable as any Nonmutating).ƒ() // "nonmutating"
1 Like

As an implementation detail, this is not “overriding”; it’s a distinct requirement. This is probably not observable in the surface language but you can see it in the generated code. (The standard library has some explicit overriding protocol requirements for code size reasons.)

I’m inclined to agree with you here. A mutating API cannot satisfy a nonmutating requirement, so the compiler should at least warn on adding a mutating requirement to a nonmutating one, since as you say it does not change anything about conforming types.

4 Likes

The GRDB SQLite wrapper defines a pair of such protocols:

protocol MutablePersistableRecord {
    mutating func didInsert(_ inserted: InsertionSuccess)
    // snip
}

protocol PersistableRecord: MutablePersistableRecord {
    func didInsert(_ inserted: InsertionSuccess)
    // snip
}

The mutable version makes it possible for records to mutate on insertion, for example in order to grab their auto-incremented id:

struct Player: Encodable, MutablePersistableRecord {
    var id: Int64?
    var name: String

    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
var player = Player(id: nil, name: "Arthur")
try player.insert(db)
player.id // not nil

Records who don't want to mutate on insertion just use the non-mutating protocol:

struct Team: Encodable, PersistableRecord {
    var id: UUID
    var name: String
}
let team = Team(id: UUID(), name: "Reds")
try team.insert(db)

The non-mutating version is properly dispatched, as the snipped below reveals:

protocol Mutating {
    var property: Int { mutating get }
}

protocol NonMutating: Mutating {
    var property: Int { get }
}

struct S: NonMutating {
    var property: Int { 1 }
}

var m: some Mutating = S()
print(m.property) // prints 1
1 Like

Kinda sorta. You can override* the implementation of a mutating member but you cannot stop it from mutating.

var m: some Mutating = S() {
  willSet { fatalError() }
}
m.property // 🤯🪦

I bet you made PersistableRecord derive from MutablePersistableRecord to make something easier to write, not because you really see things that way. Yes?

My current thinking is that it will always seem arbitrary if mutating derives from nonmutating, or vice versa—either way will be confusing, and a lie. I'm always open to having my mind changed.


*

I'm sorry if I'm getting the terminology wrong—it seems close to "hiding", using the new keyword, in C#. Is it "hiding"?

I don't get your question. I do indeed see it this way, and it matches user needs, so it's the best of both world.

You can override* the implementation of a mutating member but you cannot stop it from mutating.

Yes. some Mutating forces the compiler to assume a mutating property. Now, mutating methods are allowed to mutate nothing. And the non-mutating child protocol is a way to constraint mutating methods to mutate nothing:

// var required
var m1: some Mutating = S()
m1.property

// let ok
let m2: some NonMutating = S()
m2.property
1 Like

You'll see similar subtyping with throws and async

protocol Mutating {
    var property: Int { mutating get }
}

protocol NonMutating: Mutating {
    var property: Int { get }
}

struct S1: NonMutating {
    var property: Int { 1 }
}

// ---

protocol Throwing {
    func f() throws
}

protocol NonThrowing: Throwing {
    func f()
}

struct S2: NonThrowing {
    func f() { }
}

// ---

protocol Async {
    func f() async
}

protocol NonAsync: Async {
    func f()
}

struct S3: NonAsync {
    func f() { }
}

”Hiding” seems roughly equivalent, yes. You might be able to use the unsupported attribute @_implements to see the difference: if this was a true override, both requirements would have to have the same satisfying property (because there would only be one slot in the conformance); but as written, there are two slots and thus two different properties could theoretically satisfy the two requirements.

(While here, I’ll confirm what Gwendal is saying. You’ve always been able to implement a mutating requirement with a nonmutating method, and that’s what’s happening here, in addition to implementing the nonmutating requirement on the child protocol.)

async is the same. But I don't think it's "subtyping" when you reverse the derivation. Use case?

protocol NonAsync {
  func f()
}

protocol Async: NonAsync {
  func f() async
}
protocol Nonmutating {
  var property: Void { nonmutating get }
}

protocol Mutating: Nonmutating {
  var property: Void { mutating get }
}

throws is not the same. You "Cannot override(:disappointed:) non-throwing instance method with throwing instance method".

Indeed the reversed derivations are not sound because they break the Liskov Substitution Principle. OK maybe they don't, because @jrose has explained that those are distinct requirements. But I guess we can agree this is very subtle, considering the compiler support for the sound derivation. If the compiler let them pass (the unsound ones), I also recommend opening an issue.

Doesn't MutatingCollection also shadow Collection's get-only subscript requirement with one that is get set, or are those requirements different from mutating/non-mutating method requirements?

Adding a setter is purely additive—like going from KeyPath to WritableKeyPath. It doesn't require the mental gymnastics that everything else that compiles in this thread does. You also can't derive "in the wrong direction".

protocol GetSet {
  var property: Void { get set }
}

protocol Get: GetSet  {
  var property: Void { get } // Cannot override mutable property with read-only property 'property'
}

After thinking more about this, and looking into the failure of Rethrowing protocol conformances, I wouldn't know what issue to bring up—I feel pretty disheartened about the inconsistencies, and lack of ability to represent truth. But while I can't bring myself to see e.g. nonmutating being a subtype of mutating, I can at least see how it's the same as being able to shadow a member at various levels of a protocol hierarchy.

protocol GetVoid {
  var property: Void { get }
}

protocol GetBool: GetVoid {
  var property: Bool { get }
}

protocol GetInt: GetBool {
  var property: Int { get }
}

protocol GetString: GetInt {
  var property: String { get }
}
extension GetString {
  var properties: (Void, Bool, Int, String) {
    let selfs: (some GetVoid, some GetBool, some GetInt) = (self, self, self)
    return (
      selfs.0.property,
      selfs.1.property,
      selfs.2.property,
      property
    )
  }
}

This relationship is valid, because a non-mutating type can do everything that its mutating parent does.

For example, you can implement IteratorProtocol with a class that declares a non-mutating next() method:

class MyIterator: IteratorProtocol {
    // Non-mutating: valid fulfilment 
    func next() { ... }
}

let iterator = MyIterator()
iterator.next()

// Still have to declare a mutable variable
// when the instance is seen as an `IteratorProtocol`.
var iterator: some IteratorProtocol = MyIterator()
iterator.next()

If you're ok that MyIterator is a valid implementation of IteratorProtocol, despite its non-mutating next() method, then why not make one more step, by declaring the type of all iterators that do not mutate?

protocol NonMutatingIteratorProtocol: IteratorProtocol {
    func next()
}

Thanks to NonMutatingIteratorProtocol, we become able to write generic algorithms on iterators that do not mutate.

The key point is that those iterators are still able to feed generic algorithms on regular mutating iterators. When seen a regular iterators, the non-mutating iterators will be assumed to mutate, and the compiler will do everything related to mutating methods. That will be "too much work", in some way, for those non-mutating iterators. See your "willSet" demo above. Those iterators just won't mutate when given the opportunity to do so.


What's not sound is the reverse subtyping. The compiler won't compile types that attempt at fulfilling a non-mutating requirement with a mutating method. The compiler did not notice the problem on the Mutating protocol, and this is why @jrose suggested that this would emit a warning.

protocol NonMutating { func f() }
protocol Mutating: NonMutating { mutating func f() } // missing warning

struct S: Mutating {
    // Type 'S' does not conform to protocol 'NonMutating'
    // note: candidate is marked 'mutating' but protocol does not allow it
    mutating func f() { }
}
1 Like

I am, but this is very confusing :sweat_smile:

protocol NonMutating { func f() }
protocol Mutating: NonMutating { mutating func f() }

struct S: Mutating {
    @_implements(NonMutating, f())
    func g() { print("g") }
    
    @_implements(Mutating, f())
    mutating func h() { print("h") }
}

do {
    let s: some NonMutating = S()
    s.f() // g
}
do {
    let s: some Mutating = S()
    s.f() // g
}
do {
    var s: some Mutating = S()
    s.f() // h
}
4 Likes