Search code examples
arraysswiftstructmutating-function

Why does Swift throw 'Cannot use mutating member on immutable value' in some chain calls but not others?


In Swift, I have a custom Deque structure with various mutating methods for adding and removing elements. When I chain method calls like Deque.pushBack(contentsOf: ...), I encounter the error "Cannot use mutating member on immutable value: function call returns immutable value." However, when I use the same chaining pattern on a variable like b, it works fine. Can someone explain why this happens? Is it related to the match function being a global function? How can I resolve this issue?

 struct Deque {
     private var arrayDeque: [Int] = []
     
     mutating func pushFront(_ element: Int) {
         arrayDeque.insert(element, at: 0)
     }
     
     @discardableResult
     mutating func pushFront(contentsOf elements: Int...) -> Deque {
         for element in elements.reversed() {
             pushFront(element)
         }
         return self
     }
     
     mutating func pushBack(_ element: Int) {
         arrayDeque.append(element)
     }
     
     @discardableResult
     mutating func pushBack(contentsOf elements: Int...) -> Deque {
         for element in elements {
             pushBack(element)
         }
         return self
     }
     
     @discardableResult
     mutating func popFront() -> Deque? {
         guard !isEmpty else { return nil }
         var copy = self
         copy.arrayDeque.removeFirst()
         return copy
     }
     
     @discardableResult
     mutating func popBack() -> Deque? {
         guard !isEmpty else { return nil }
         var copy = self
         copy.arrayDeque.removeLast()
         return copy
     }
     
     var isEmpty: Bool {
         return arrayDeque.isEmpty
     }
     
     var count: Int {
         return arrayDeque.count
     }
     
     func peekFront() -> Int? {
         return arrayDeque.first
     }
     
     func peekBack() -> Int? {
         return arrayDeque.last
     }
 }

 func match(_ xs: Deque) -> Bool { ... }

 // gives me error
 let a = match(Deque().pushBack(contentsOf: 1, 2, 1, 2, 1))

 // works fine
 var b = Deque()
 let b2 = b.pushBack(contentsOf: 1, 2, 1, 2, 1)
 let b3 = match(b2)

error: Cannot use mutating member on immutable value: function call returns immutable value

I'm trying to understand why Swift behaves differently in these scenarios and how I can ensure consistent behavior when chaining method calls on custom data structures. Any insights or explanations would be greatly appreciated! (I'm new to this, if you can explain in basic terms it would be great, thanks)

Also chatGPT explanation: "Mutability of the Instance: If you're chaining mutable methods on an instance, ensure that the instance itself is mutable (declared with var). Otherwise, you won't be able to apply the changes made by the methods.", and also something about how "is due to the fact that in Swift, when you chain method calls like in example a, the intermediate result is immutable."


Solution

  • There is no technical reason why you are not allowed to do this, but in most cases. calling a mutating function on a return value is most likely a mistake. (See also) Consider a rather contrived example:

    var deque = Deque()
    func returnADeque() -> Deque { return deque }
    
    // suppose this is allowed
    returnADeque().pushBack(1)
    

    This will not actually modify the global variable deque. Deque is a struct (a value type), so returnADeque returns a copy of it.

    The same applies to initialisers. From the caller's perspective, an initialiser is just a function that returns an instance of whatever type it is initialising. Suppose pushBack does not return self, then Deque().pushBack(contentsOf: 1, 2, 1, 2, 1) would not be doing any useful work. You create a Deque, then push some numbers onto it, then you discard it, not using any of the things you pushed.

    To do this chaining, you can change Deque to a class (a reference type), which has different semantics. You can also make two versions of each function - one that is not mutating and returns self for chaining, and one that is mutating and doesn't return self.

     mutating func pushBack(contentsOf elements: Int...) {
         for element in elements {
             pushBack(element)
         }
     }
    
    func pushedBack(contentsOf elements: Int...) -> Deque {
        var copy = self
        // normally you can reuse the mutating function here, but because pushBack
        // takes a variadic Int..., it is not possible in this case
        for element in elements {
            copy.pushBack(element)
        }
        return copy
    }
    

    The method that allows chaining should use the past particle of the verb, to follow the naming conventions used in the standard library. shuffle() vs shuffled(), reverse() vs reversed() etc.