Clarify behavior of mutating functions when they return values and are chained. #401
Labels
docs: Book
/book section of the docs: Guides, Cheatsheets, and a streamlined sequence of educational materials
The behavior of
mutates
functions needs to be clarified, specially of thosemutates
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,
will mutate variable
s.a
to3
in this code snippet:because the
incr
functions changes theself
struct.Since mutable functions change their
self
argument (something that simpleextends
functions do not do), some users may believe thatmutates
functions pass theirself
argument by reference. This is NOT what happens. All functions in Tact pass their arguments by value, butmutates
functions carry out a special step once they finish execution: they assign the result in theirself
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 codes.incr()
above, what happens is the following:incr
is called by instantiatingself
with a copy ofs
. Denote the copy ass'
.incr
changes thea
field ins'
to3
.incr
finishes execution, so it assigns tos
whatever value is currently stored inself
, which iss'
but with3
in itsa
field.s
is nowA {a: 3}
.So far, a user that thinks that
self
is passed by reference inmutates
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 theincr
function as follows:The above may be written by a user that thinks that
self
is passed by reference, in an attempt to chain theincr
function:The user incorrectly thinks: "Since
self
is passed by reference, when the first call toincr
finishes,s.a = 3
. Then, the modifieds
is given (by reference) to the next call ofincr
, and so on". This user will conclude thats.a = 5
in the above code snippet. This is NOT what happens.In the above code snippet,
s.a
will actually be3
(not5
). 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
.incr
, which iss1
, is given as input to the second call ofincr
.incr
instantiatesself
with a copy ofs1
. Denote its2
.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
.Note that in the above steps,
s
is only modified by the first call toincr
. Hence,s.a = 3
. If the user actually wants to modifys
with the value returned by the third call toincr
, variables
needs to be explicitly assigned: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.,because the meaning is clearer.
The
incr
function is a bit confusing because it actually returns TWO values: theself
which is automatically assigned back into the variable calling the mutate function, and the value in thereturn
statement.The fact that
mutates
functions can return two values is better exemplified with the following code:What do you think is the final value of the variables in this code snippet?
Answer:
s = 6
andt = 4
.Note 1: When there is a chain of mutates functions, like
s.mutFun1().mutFun2()....
, Tact will assign theself
value back intos
only in functionmutFun1
, because there is no variable to assign back in functionsmutFun2
, and so on. However, the return value ofmutFun1
will be given as input tomutFun2
. The return value ofmutFun2
tomutFun3
, and so on.The text was updated successfully, but these errors were encountered: