|On this page…|
When calling a procedure with some arguments, you expect the procedure to assign these values for its local variables and perform some computations with those variables. For example, this procedure divides any number that you pass to it by 10:
f := x -> (x := x/10):
In this example, x is a local variable of f. When you call f with any value, the procedure assigns that value to the local variable x, uses it to compute the result, and then destroys the local variable x freeing allocated memory:
x := 10: f(x), x
Although the value of the local variable x changes to 1 inside the procedure and then gets destroyed, the value of the global variable x remains the same. Therefore, you can conclude that the procedure does not access the actual memory block that contains the value of x.
When you call this procedure, MuPAD® allocates a new memory block and copies the value of x to that block. While the procedure executes, the system associates the local variable x with this new memory block. At the end of the procedure call, it frees this memory block. The memory block that contains the value of the global variable x does not change.
The strategy of copying values of procedure arguments to new memory blocks and referring to these blocks during the procedure calls is known as calling by value. Most MuPAD functions use this strategy.
Since calling by value creates extra copies of data, it can be inefficient in terms of memory management. Creating extra copies of large data sets, especially when your procedure calls are recursive or nested can significantly reduce free memory on your computer. In many programming languages, calling by value always means copying data, even when a procedure does not change that data.
MuPAD uses lazy copying to avoid creating unnecessary copies of data. When you call a procedure, MuPAD does not allocate new memory blocks right away. Instead, it links local variables to memory blocks where the arguments of the procedure are stored. The system always counts how many objects are linked to the same memory block. When a procedure modifies the value of a local variable, MuPAD checks if there are other objects linked to the same memory block, and creates a copy only if necessary.
For example, when you call f(x), MuPAD points both global variable x (DOM_IDENT) and local (DOM_VAR) variable x to the same memory block. Only when the local variable x changes its value, MuPAD allocates a new memory block for it.
Typically, when you call a MuPAD procedure with some arguments, the system uses the calling-by-value approach and creates copies of the values of these arguments. This approach prevents procedures from modifying objects passed to a procedure as arguments. For some functions, such as assignment, MuPAD uses the calling-by-reference approach. In a call by reference, the system does not copy the values of arguments to new memory blocks. Instead, it copies references (pointers) to these arguments.
Some programming languages let you specify which approach to use for each particular function. MuPAD does not offer specific language constructs for calling by reference. Nevertheless, you can still call by reference in MuPAD.
Note: For experienced MuPAD users, objects with reference semantics can behave unexpectedly. Be careful when exposing reference semantics to your users because it can be confusing.
Suppose your task requires a function call to be able to change the values of its arguments. The simple strategy is to return a modified copy of the arguments and overwrite the original object by using assignment. For example, replace matrix A with its upper row echelon form:
A := linalg::hilbert(3)
A := linalg::gaussElim(A)
When working with large data sets and deep nested calls, this strategy can cause memory problems. Check the profiling report to see if your implementation has such problems. For details, see Profiling Your Code.
You also can achieve the calling-by-reference effect in MuPAD using:
Instead of passing data as arguments of a procedure, you can use a local variable of the outer procedure to store the data. For the inner procedure, this variable is not local. Therefore, the inner procedure can change the value of that variable, and the variable is not destroyed after the inner procedure is executed:
f := proc(x) local n, incr; begin n := 0; incr := () -> (n := n + 1); while x > n do incr(); end_while; end_proc:
This approach does not fit many programming tasks, but it is recommended wherever you can apply it. It is the simplest and most readable approach to get the calling-by-reference effect in MuPAD.
When working with domains, you also can use the approach of having the actual data in a closure. For example, instead of storing the actual data in the objects, you can store functions that access the data:
domain d local get, set; inherits Dom::BaseDomain; new := proc(x) option escape; begin new(dom, () -> x, y -> (x := y)); end; incr := x -> set(x, get(x) + 1); print := x -> get(x); begin get := x -> extop(x, 1)(); set := (x, y) -> extop(x, 2)(y); end_domain: e := d(4)
See Closures for more details.
You can implement the calling-by-reference approach in your code by using domains as tables with reference effects. (Using domains as tables is unrelated to object-oriented programming.) The following example demonstrates this strategy:
domain dd local get, set; inherits Dom::BaseDomain; new := proc(x) local d; begin d := newDomain(genident()); d::number := x; new(dom, d); end; get := (x, entry) -> slot(extop(x, 1), entry); set := (x, entry, value) -> slot(extop(x, 1), entry, value); incr := x -> (dom::set(x, "number", dom::get(x, "number") + 1); x); print := x -> dom::get(x, "number"); end_domain: e := dd(4)
f := plot::Function2d(sin(x), x=-PI..PI): f::Color := RGB::Green:
The context function and option hold also let you implement the calling be reference effect in your procedures. option hold prevents the procedure from evaluating its arguments before executing the code in the procedure body. The context function lets you perform operations as if they occur in the calling procedure.
For example, in this code option hold prevents the incr procedure from evaluating its argument. Therefore, the system does not copy the value of x to a new memory block:
incr := proc(x) option hold; begin context(hold(_assign)(x, x + 1)); end_proc: operator("++", incr, Prefix, 500):
While executing this procedure, the system performs the assignment operation in the context of the procedure f (the calling procedure for incr). Thus, incr changes the value of the argument, n, with which it was called:
f := proc(x) local n; begin n := 0; while x > n do ++n; end_while; end_proc:
If you use the ++ operator on an unchangeable object, MuPAD throws an error. For example, you cannot assign the value 2 to the value 1:
Error: The left side is invalid. [_assign]
This error message does not mention incr because the error occurs in the assignment which is performed in a different evaluation context. The incr procedure behaves essentially like a dynamic macro.