Description
The behavior of mutates
functions needs to be clarified, specially of those mutates
that return a value and those that can be chained.
The Tact Book states that "mutable functions are performing mutation of a value replacing it with an execution result. To perform mutation, the function must change the self
value".
So,
struct A {
a: Int
}
extends mutates fun incr(self: A) {
self.a += 1;
}
will mutate variable s.a
to 3
in this code snippet:
let s = A {a: 2};
s.incr(); // s.a is now 3
because the incr
functions changes the self
struct.
Since mutable functions change their self
argument (something that simple extends
functions do not do), some users may believe that mutates
functions pass their self
argument by reference. This is NOT what happens. All functions in Tact pass their arguments by value, but mutates
functions carry out a special step once they finish execution: they assign the result in their self
variable back into the variable that the mutable function was called upon [there is an exception to this, see Note 1 below]. For example, in the code s.incr()
above, what happens is the following:
- Function
incr
is called by instantiatingself
with a copy ofs
. Denote the copy ass'
. incr
changes thea
field ins'
to3
.- At this moment,
incr
finishes execution, so it assigns tos
whatever value is currently stored inself
, which iss'
but with3
in itsa
field. - Hence,
s
is nowA {a: 3}
.
So far, a user that thinks that self
is passed by reference in mutates
functions would get the same correct conclusions as one that actually knows how mutates functions work, because the above example is simple. The confusion starts with mutable functions that return values, specially when those functions are chained. For example, let us modify the incr
function as follows:
extends mutates fun incr(self: A): A {
self.a += 1;
return self;
}
The above may be written by a user that thinks that self
is passed by reference, in an attempt to chain the incr
function:
let s = A {a: 2};
s.incr().incr().incr();
The user incorrectly thinks: "Since self
is passed by reference, when the first call to incr
finishes, s.a = 3
. Then, the modified s
is given (by reference) to the next call of incr
, and so on". This user will conclude that s.a = 5
in the above code snippet. This is NOT what happens.
In the above code snippet, s.a
will actually be 3
(not 5
). The reason is as follows:
incr
is called by instantiatingself
with a copy ofs
. Denote the copy bys1
.incr
modifiess1.a = 3
and returnss1
.incr
assigns back tos
whatever is in itsself
variable, which iss1
withs1.a = 3
.- But then, the result of
incr
, which iss1
, is given as input to the second call ofincr
. - The second call of
incr
instantiatesself
with a copy ofs1
. Denote its2
. - The second call finishes modifying
s2.a = 4
. The step that assignsself
back into "s1
" is ignored this time becauses1
is not an actual variable [see Note 1 below]. Butincr
gives the returned value (which iss2
withs2.a = 4
) as input to the third call toincr
. - And so on.
Note that in the above steps, s
is only modified by the first call to incr
. Hence, s.a = 3
. If the user actually wants to modify s
with the value returned by the third call to incr
, variable s
needs to be explicitly assigned:
s = s.incr().incr().incr();
In order to avoid the above confusing behavior, it seems to me that a better approach would be simply to avoid mutates
chains by breaking the chain into independent steps, i.e.,
s.incr();
s.incr();
s.incr();
because the meaning is clearer.
The incr
function is a bit confusing because it actually returns TWO values: the self
which is automatically assigned back into the variable calling the mutate function, and the value in the return
statement.
The fact that mutates
functions can return two values is better exemplified with the following code:
// Increments the argument, but returns the previous integer to the argument
extends mutates fun incr(self: Int): Int {
let prev = self - 1;
self += 1;
return prev;
}
What do you think is the final value of the variables in this code snippet?
let s = 5;
let t = s.incr();
Answer: s = 6
and t = 4
.
Note 1: When there is a chain of mutates functions, like s.mutFun1().mutFun2()....
, Tact will assign the self
value back into s
only in function mutFun1
, because there is no variable to assign back in functions mutFun2
, and so on. However, the return value of mutFun1
will be given as input to mutFun2
. The return value of mutFun2
to mutFun3
, and so on.