tact-docs
tact-docs copied to clipboard
Clarify behavior of mutating functions when they return values and are chained.
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
incris called by instantiatingselfwith a copy ofs. Denote the copy ass'. incrchanges theafield ins'to3.- At this moment,
incrfinishes execution, so it assigns toswhatever value is currently stored inself, which iss'but with3in itsafield. - Hence,
sis 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:
incris called by instantiatingselfwith a copy ofs. Denote the copy bys1.incrmodifiess1.a = 3and returnss1.incrassigns back toswhatever is in itsselfvariable, which iss1withs1.a = 3.- But then, the result of
incr, which iss1, is given as input to the second call ofincr. - The second call of
incrinstantiatesselfwith a copy ofs1. Denote its2. - The second call finishes modifying
s2.a = 4. The step that assignsselfback into "s1" is ignored this time becauses1is not an actual variable [see Note 1 below]. Butincrgives the returned value (which iss2withs2.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.