Unexpected behaviour of mutating call used as an argument

Hello,

I am trying to understand the behaviour of the following code:

struct A {
     var i: Int = 0

     func output(_ label: String) {
         print("\(label): \(i)")
     }

     mutating func frob() -> String {
         i = i + 1
         return "a"
     }

     mutating func test() {
         output(frob()) // prints "a: 0"
         output("b")    // prints "b: 1"
     }
 }

 var a = A()
 a.test()

I expected the first line of output to be 'a: 1' because the argument to output(_:) has already been evaluated (and therefore mutated self) before output(_:) is called. Instead it seems as though it is still seeing the previous value at that point.

Can somebody explain how this code is actually being evaluated?

2 Likes

From the docs:

An in-out parameter has a value that is passed in to the function, is modified by the function, and is passed back out of the function to replace the original value.

inout should be correlated with a reference carefully, they don't share the same principle. Once that is settled – a mutating method is a function with self passed as an inout parameter.

Hasn't self already been passed into frob() as an inout parameter, and then passed back out to replace its original value, by the time the body of the first call to output(_:) is executed?

Notice you are calling frob from a mutating method (test). It has to be mutating because you are mutating self within the method via frob. All changes made within test won't take place until you return.

In that case, why does the second call to output(_:) (also within the same call to test()) see the modified value?

I understand that the changes will not be visible from the outside until test() returns, but surely changes made within test() must be visible within the test() call.

Yes, you're right. Wierd.

If we put in the implicit self arguments, we get this:

self.output(self.frob()) // prints "a: 0"

If we record the expressions that need to be evaluated, we get something like

$0 = self // the first one on the line
$1 = self // the second one on the line, passed inout
$2 = $1.frob() // modifies $1 and returns "a"
self = $1 // finish the inout access
$0.output($2) // uses the cached value of 'self'

That explains the behavior you're seeing.


For fun, let's see what happens if we make output mutating as well:

$1 = self // the second one on the line, passed inout
$2 = $1.frob() // modifies $1 and returns "a"
self = $1 // finish the inout access
$3 = self // the first one on the line, now passed inout
$3.mutatingOutput($2)
self = $3 // finish the inout access

In this case, the access to the leftmost self has to be delayed until just before mutatingOutput is actually called, to avoid overlapping with the mutation that happens when calling frob. I can definitely see this being considered inconsistent, but it doesn't come up much in practice. The rule is "evaluate all non-inout arguments left to right, then begin access to each inout argument left-to-right".

6 Likes

And just for fun, I tried this and got an error:

func test(_ a: inout A?) {
	a?.mutatingOutput(a!.frob()) // Overlapping accesses to 'a', but modification requires exclusive access; consider copying to a local variable
	a?.mutatingOutput("b")
}

I guess in this case it can't reorder the first access to a to after the second one because the second one is conditional on the first succeeding.

1 Like

Yeah, this is where it'd be nice to have an if inout binding form to nest the unwrapped access inside a single access to the outer optional.

2 Likes