Reconsidering the "cannot use mutating member on immutable value" error

Hello,

Mutating methods can have the compiler emit "cannot use mutating member on immutable value" errors. For example:

// Error: cannot use mutating member on immutable value
let e = [1, 2, 3].makeIterator().next()

To avoid this error, one has to use a mutable variable:

// OK
var i = [1, 2, 3].makeIterator()
let e = i.next()

Yet, sometimes, one does not care about the mutated value, but only about the result, or the side effects, of the mutating method. In this case, the mandatory declaration of the mutable variable hinders code legibility, and is only a matter of pleasing the compiler.

Here are some other examples that use imaginary of actual APIs:

// Wish we could ignore the modified scanner that has gone
// past the first line.
//
// But error: cannot use mutating member on immutable value
let firstLine = Scanner(string: "...").nextLine()

// Wish we could ignore the modified Player that is given an id after
// its insertion in the database.
//
// But error: cannot use mutating member on immutable value
try Player(id: nil, name: "Arthur").insert(database)

This error has another undesired consequence: mutating methods can not be used in expressions. One has to use several statements instead. Good bye privileged single-line closures. Good bye streamlined functional expressions.

Do you think that it would be worth working on relaxing the "cannot use mutating member on immutable value" error? Assuming it would not bring unsolvable inconsistencies in the language, of course.

If so, we could also look at the "cannot pass immutable value as inout argument" error, which is quite similar in that it also prevents one from ignoring irrelevant mutations.

6 Likes

Your examples all call a mutating method on the result of an initializer (or literal, or function call). Would it be sufficient to let initializers (and other functions) return mutable values?

Thanks for your question. A good opportunity to clarify the desired changes :-)

Indeed, not all "cannot use mutating member on immutable value" should be avoided:

// Would become allowed:
makeValue(...).mutatingFunc()
Value(...).mutatingFunc()

// Would remain forbidden:
let value = Value(...)
value.mutatingFunc()

extension Value {
    func foo() {
        mutatingFunc()
        self.mutatingFunc()
        super.mutatingFunc()
    }
}

I did try an exhaustive study of the grammar:

Grammar

Unless I missed some, mutating method and subscripts are covered by those rules:

  • self-method-expression → self . identifier
  • self-subscript-expression → self [ function-call-argument-list ]
  • superclass-method-expression → super . identifier
  • superclass-subscript-expression → super [ function-call-argument-list ]
  • explicit-member-expression → postfix-expression . identifier­generic-argument-clause­opt
  • explicit-member-expression → postfix-expression . identifier ( argument-names )
  • subscript-expression → postfix-expression [ function-call-argument-list ]
  • function-call-expression → postfix-expression function-call-argument-clause
  • function-call-expression → postfix-expression function-call-argument-clause(opt) trailing-closure

We don't want self and super mutations to be ignored: the first grammar rules are excluded from error relaxing. Let's look at postfix-expression:

  • postfix-expression → primary-expression
  • postfix-expression → postfix-expression postfix-operator
  • postfix-expression → function-call-expression
  • postfix-expression → initializer-expression
  • postfix-expression → explicit-member-expression
  • postfix-expression → postfix-self-expression
  • postfix-expression → subscript-expression
  • postfix-expression → forced-value-expression
  • postfix-expression → optional-chaining-expression

We get closer:

  • postfix-expression postfix-operator: relaxed

    value+++.mutatingFunc() // OK (new)
    
  • function-call-expression: relaxed

    let v = makeValue().mutatingFunc() // OK (new)
    
  • initializer-expression: relaxed

    Value().mutatingFunc() // OK (new)
    
  • explicit-member-expression: it gets complicated. Plus we have to deal with properties with or without a setter.

    let value1 = Value()
    value1.foo.mutatingFunc()   // Error (no change)
    
    var value2 = Value()
    value2.foo.mutatingFunc()   // OK iff `foo` is a mutable property (no change)
    
    Value().foo.mutatingFunc()  // OK (new)
    

    No clear rule yet for explicit-member-expression.

  • postfix-self-expression: it gets complicated

    let value1 = Value()
    value1.self.mutatingFunc()   // Error (no change)
    
    var value2 = Value()
    value2.self.mutatingFunc()   // OK (no change)
    
    Value().self.mutatingFunc()  // OK (new)
    

    No clear rule yet for postfix-self-expression.

  • subscript-expression: it gets complicated. Plus we have to deal with subscripts with or without a setter.

    let value1 = Value()
    value1[0].mutatingFunc()   // Error (no change)
    
    var value2 = Value()
    value2[0].mutatingFunc()   // OK iff the subscript has a setter (no change)
    
    Value()[0].mutatingFunc()  // OK (new)
    

    No clear rule yet for subscript-expression.

  • forced-value-expression: it gets complicated

    Value(...)!.mutatingFunc()  // OK (new)
    
    let value1 = Value()
    value1.foo!.mutatingFunc()   // Error (no change)
    
    var value2 = Value()
    value2.foo!.mutatingFunc()   // OK iff `foo` is a mutable property (no change)
    
    Value().foo!.mutatingFunc()  // OK (new)
    

    No clear rule yet for forced-value-expression.

  • optional-chaining-expression: it gets complicated

    Value(...)?.mutatingFunc()  // OK (new)
    
    let value1 = Value()
    value1.foo?.mutatingFunc()   // Error (no change)
    
    var value2 = Value()
    value2.foo?.mutatingFunc()   // OK iff `foo` is a mutable property (no change)
    
    Value().foo?.mutatingFunc()  // OK (new)
    

    No clear rule yet for optional-chaining-expression.

  • primary-expression

  • primary-expression → identifier generic-argument-clause(opt): out of scope

    Array<Int>.mutatingFunc() // does not exist
    
  • primary-expression → literal-expression: it's debatable

    true.toggle()          // ?
    [1, 2, 3].removeLast() // ?
    
  • primary-expression → self-expression: error

    extension Value {
        func foo() {
            mutatingFunc()      // error
            self.mutatingFunc() // error
        }
    }
    
  • primary-expression → superclass-expression: error

    extension Value {
        func foo() {
            super.mutatingFunc() // error
        }
    }
    
  • primary-expression → closure-expression: out of scope

    { 1 }.mutatingFunc() // does not exist
    
  • primary-expression → parenthesized-expression: same as the inner expression

    (Value()).mutatingFunc() // OK
    
  • primary-expression → tuple-expression: out of scope

    (1, 2, 3).mutatingFunc() // does not exist
    
  • primary-expression → implicit-member-expression: out of scope, I believe

    .someValue.mutatingFunc() // Meaningful?
    
  • primary-expression → wildcard-expression: out of scope

    _.mutatingFunc()
    
  • primary-expression → key-path-expression: out of scope

    \Value.foo.mutatingFunc() // does not exist
    
  • primary-expression → selector-expression: out of scope

    #selector(foo(_:)).mutatingFunc() // does not exist
    
  • primary-expression → key-path-string-expression

Some cases look like they can be handled at the grammar level: function-call-expression, initializer-expression, others.

Some other cases are more subtle: explicit-member-expression, subscript-expression, etc.

Maybe the grammar is not the proper level. Concepts like "rvalues" and "lvalues" are closer to the correct level, but Swift doesn't use the C terminology, and I'm not yet familiar enough with Swift for me to use the proper terms.

Naively, I'd say that the error could be avoided when the compiler can generate a temporary mutable value that doesn't shadow any accessible value (through variable identifiers, properties, etc).

Internally, I think it does? I think the proposal can be summed up as making rvalues mutable by default.