MuPAD User's Guide - Tabs - Programming
MuPAD User's Guide - Tabs - Programming
Programming Fundamentals
4-2
Data Type Definition
Domain Types
MuPAD stores all objects as elements of particular domains. There are two types of
domains in MuPAD: basic domains and library domains. The system includes the basic
domains written in C++. The names of the basic domains start with DOM_. For example,
the domain of integers DOM_INT, the domain of rational numbers DOM_RAT, and the
domain of identifiers DOM_IDENT are basic domains. Most of the basic domains available
in MuPAD are listed in “Basic Domains”.
The system also includes library domains, such as the domain Dom::Matrix() of
matrices, the domain Dom::ArithmeticalExpression of arithmetical expressions, the
domain Dom::Interval of floating-point intervals, and the domain stats::sample of
statistical samples. Many library domains are listed in “Library Domains”. Other library
domains are listed in the corresponding libraries. For example, you can find the library
domain solvelib::BasicSet of the basic infinite sets under the “Utilities for the
Solver” category. The library domains are written in the MuPAD programming language.
You also can create your own library domains.
Overloading works differently for the basic domains and the library domains. The system
can overload any method of a library domain. For basic domains, the system overloads
only some methods.
Expression Types
The basic domain DOM_EXPR includes MuPAD expressions, such as expressions created
by arithmetical or indexed operators, statements, and function calls. MuPAD classifies
the elements of the domain DOM_EXPR further by defining expression types. Expression
types provide more detailed information about a particular expression. For example, you
might want to know whether the expression is an arithmetical expression, such as a sum
or a product, a Boolean expression, a function call, or some other type of expression. For
these expressions, the domtype function returns their domain type DOM_EXPR:
domtype(a + b), domtype(a*b),
4-3
4 Programming Fundamentals
To find the expression types of these expressions, use the type function:
If an operator or a function has a “type” slot, the type function returns the string value
stored in that slot. For example, the “type” slot of the addition operator contains the
string value “_plus”, the “type” slot of the multiplication operator contains “_mult”, the
“type” slot of the sine function contains “sin”, and so on.
An expression can include more than one operator. Typically, MuPAD associates the
expression type of such expressions with the lowest precedence operator. If you visualize
an expression as an expression tree, the lowest precedence operator appears at the
root of that tree. See Visualizing Expression Trees for more information. For example,
consider the expression a + b*c. When evaluating this expression, the system performs
multiplication, and then performs addition. Therefore, the addition operator is the lowest
precedence operator in the expression. This operator determines the expression type:
type(a + b*c)
If the lowest precedence operator in the expression does not have a “type” slot, the type
function returns the string “function”:
type(f(a^2 + a + 2))
The domtype and type functions return the same results for elements of most MuPAD
domains:
4-4
Data Type Definition
domtype(5/7), type(5/7);
domtype(1.2345), type(1.2345);
domtype(a), type(a);
If your code relies on the assumption that an object belongs to a particular domain type
or expression type, verify that assumption before executing the code. To test whether a
MuPAD object belongs to a particular type, use the testtype function. Use this function
to test both domain types and expression types:
testtype(a + b, DOM_EXPR), testtype(a + b, "_plus")
4-5
4 Programming Fundamentals
When you create more complicated data structures, such as sets, lists, arrays, matrices,
procedures, and so on, the syntax that you use to create these structures is a shortcut to
the domain constructors. For example, matrix is a shortcut for the domain constructor
Dom::Matrix with the default ring Dom::ExpressionField().
Also, when defining your own procedure, you can specify the types of arguments accepted
by that procedure. In this case, for every call to that procedure, MuPAD automatically
checks the types of provided arguments. For more information, see Checking Types of
Arguments.
The domtype function returns the name of a domain to which an object belongs:
domtype([a, b, c])
When choosing a data structure for a new object, try to answer these questions:
• Which features are essential to the new object? For example, some structures
keep the initial order of elements, while other structures can change the order.
Another example is that you can create multidimensional arrays, but you cannot
create MuPAD matrices with more than two dimensions.
• Which functions do you want to use on that object? Each MuPAD function
accepts only objects of particular domain types. If an object that you pass to a function
is not one of the acceptable domain types for that function, the function issues an
error. To determine the domain types acceptable for a particular function, see the
help page for that function. For example, you cannot use standard mathematical
operations for MuPAD arrays.
If you already created an object, and then realized that it must belong to another domain
type, try to convert the domain type of the object. See Converting Data Types.
4-6
Convert Data Types
In this section...
“Use the coerce Function” on page 4-8
“Use the expr Function” on page 4-9
“Use Constructors” on page 4-11
When creating new objects in MuPAD, the best practice is to consider which domain
the object must belong to. Choosing the correct data type from the beginning helps you
avoid unnecessary conversions. Nevertheless, some computation tasks require data
types conversions. For such tasks, MuPAD enables you to convert the elements of some
domains to elements of other domains.
To convert an object to a different domain type, use one of the following alternatives.
Some of the alternatives do not work for conversions between particular domains.
• “Use the coerce Function” on page 4-8. Call the coerce function to convert an
object to an element of a specified domain. The coerce function can call the convert,
convert_to, and coerce domain methods. If the conversion is not possible or if none
of these methods is implemented for the domains participating in conversion, the
coerce function returns FAIL.
• “Use the expr Function” on page 4-9. Call the expr function to convert an object
to an element of a basic domain. The expr function calls the expr method. If the
conversion is not possible or if the expr method is not implemented for the specified
domains, the expr function returns FAIL.
• “Use Constructors” on page 4-11. Call the domain constructor of the domain to
which you want to convert an object. This approach typically works, but it can fail in
some cases.
• Calling the conversion methods of a domain directly. To use this approach, you must
know which conversion methods are implemented for the participating domains. This
alternative is not recommended. Use the coerce or expr function instead.
Note: If you implement a new domain, consider implementing the conversion methods for
that domain.
4-7
4 Programming Fundamentals
domtype(L), domtype(S)
Results of the conversion can depend on the original domain type. For example, create an
array and a matrix with the same number of elements and equal dimensions:
M := matrix(2, 3, [1, 2, 3, 4, 5, 6]);
A := array(1..2, 1..3, [1, 2, 3, 4, 5, 6])
Verify that matrix M belongs to the domain Dom::Matrix(), and array A belongs to the
domain DOM_ARRAY:
domtype(M), domtype(A)
Use the coerce function to convert both objects to elements of the domain DOM_LIST.
The coerce function converts matrix M to a nested list, where the inner lists represent
the rows of the original matrix. At the same time, coerce converts the array A to a flat
list:
4-8
Convert Data Types
LM := coerce(M, DOM_LIST);
LA := coerce(A, DOM_LIST)
domtype(LM), domtype(LA)
delete L, S, M, A, LM, LA
The expr function tries to find the simplest basic domain to which it can convert a given
object. The function also can convert an element of a more complicated basic domain
to the simpler basic domain. For example, it converts the polynomial represented by a
single variable x to the identifier x:
y := poly(x);
expr(y)
The original object y belongs to the domain of polynomials. The result of conversion
belongs to the domain of identifiers DOM_IDENT:
4-9
4 Programming Fundamentals
domtype(y), domtype(expr(y))
If you call the expr function for a more complicated polynomial, the function converts
that polynomial to the expression:
p := poly(x^2 + x + 2);
expr(p)
Again, the original polynomial belongs to the domain of polynomials. This time, the
result of the conversion belongs to the domain of expressions:
domtype(p), domtype(expr(p))
MuPAD can apply the expr function to an object recursively. If an object contains terms
that belong to library domains or complicated basic domains, expr also tries to convert
those terms elements of simpler basic domains. For example, if the polynomial p is an
element of a list, applying the expr function to the list converts the polynomial p to the
expression:
4-10
Convert Data Types
The expr function converts a matrix of the library domain Dom::Matrix() to an array
of the basic kernel domain DOM_ARRAY:
domtype(M), domtype(expr(M))
The expr function converts an element of the series domain to the expression of a basic
domain DOM_EXPR. Typically, the order of terms in the resulting expression changes:
s := series(exp(x), x);
expr(s)
domtype(s), domtype(expr(s))
delete y, p, M, s
Use Constructors
Instead of calling the conversion functions, you can use the constructor of a domain to
which you want to convert an object. This approach works for most conversions, but it
can fail in some cases.
4-11
4 Programming Fundamentals
Calling the constructor of the required domain is often the simplest way to convert an
object to the element of that domain. Suppose you want to convert the following list L to a
matrix:
L := [1, 2, 3, 4, 5, 6]
To convert a list to a matrix, call the matrix constructor matrix. Use the arguments
of the matrix constructor to specify the dimensions of the required matrix and the list
L of the elements of a matrix. The resulting object is the 2×3 matrix of the domain
Dom::Matrix():
matrix(2, 3, L);
domtype(%)
Dom::Matrix()(2, 3, L);
domtype(%)
delete L
4-12
Define Your Own Data Types
TempF := newDomain("TempF"):
Currently the new domain does not have any elements. To create an element of a
domain, use the new function. For example, create the new element x of the domain
TempF:
x := new(TempF, 15)
To verify that the object x belongs to the domain TempF, use the testtype function. You
can also use the domtype or type function:
extop(x)
Suppose you want the new method of the domain TempF to check whether an input
argument is a valid temperature measurement. In this case, redefine the new method for
your domain TempF:
4-13
4 Programming Fundamentals
The redefined method checks that an input argument is a number. It also checks that
the provided temperature is not below absolute zero. Absolute zero is zero degrees on the
Kelvin temperature scale, which is equivalent to -459.67 degrees on the Fahrenheit scale.
The redefined method creates new elements in degrees Fahrenheit. Call the method
TempF::new directly or use the shorter syntax TempF(newElement):
TempF::new(0), TempF(32)
The TempF::new method also ensures that all new elements represent valid
temperatures:
TempF(-500)
TempF(1 + 2*I)
As a next step, improve the output format for the elements of your new domain. To
change the format that MuPAD uses for displaying the elements of a domain, redefine
the print method for that domain. For example, when displaying elements of the
domain TempF::new, show temperature measurements followed by the units of
measurements (degrees Fahrenheit):
TempF::print := proc(TempF)
begin
4-14
Define Your Own Data Types
expr2text(extop(TempF, 1)).Symbol::deg."F"
end_proc:
Suppose you want to perform additions for the elements of the new domain. By default,
arithmetical operations treat the elements of a new domain as identifiers:
TempF(75) + TempF(10)
You can implement arithmetical operations for your domain. For example, define
addition for the domain TempF. The result of addition must also belong to the domain
TempF:
TempF::_plus := proc()
local elements;
begin
elements:= map([args()], op, 1);
new(TempF, _plus(op(elements)))
end_proc:
The system add the elements of your new domain the same way as it adds numbers. The
system also displays the degrees Fahrenheit unit for the resulting sum:
TempF(75) + TempF(10)
delete x
4-15
4 Programming Fundamentals
The args function lets you access the arguments of a current procedure call. The
call args(i), where i is a positive integer, returns the ith argument of the current
procedure call. The call args(0) returns the total number of arguments in the current
procedure call. For example, the following procedure computes the sum of its arguments
in each procedure call:
f := proc() begin
_plus(args(i) $ i = 1..args(0)):
end_proc:
Also, you can access the whole sequence of arguments or any subsequence of that
sequence. For example, the following procedure prints all arguments used in the current
call. If the current call uses three or more arguments, the procedure also prints its first
three arguments:
g := proc() begin
print(Unquoted, "all arguments" = args()):
if args(0) > 2 then
4-16
Access Arguments of a Procedure
When you pass arguments to a procedure, MuPAD evaluates these arguments. Then
the system creates a local variable for each formal parameter specified in the procedure
definition. The system assigns evaluated arguments to these local variables. Parameters
outside the procedure do not change. For example, assign a new value to a formal
parameter inside the procedure:
h := proc(a) begin
a := b;
print(args()):
end_proc:
Assigning a new value to a formal parameter inside a procedure does not affect the
parameter itself. This assignment affects the result returned by args. For example, if
you pass any argument to the procedure h, that argument changes to a variable b inside
the procedure:
h(100)
The formal parameter a does not change its value outside the procedure:
a
4-17
4 Programming Fundamentals
delete f, g, h
4-18
Test Arguments
Test Arguments
In this section...
“Check Types of Arguments” on page 4-19
“Check Arguments of Individual Procedures” on page 4-21
f:= proc(k:DOM_INT)
begin
sin(PI*k/4)
end_proc:
Therefore, only an integer number is a valid first argument for this procedure:
f(1)
The system compares the type of the formal parameter k and the type of an argument
passed to the procedure. If the first argument that you pass to the procedure f is not an
integer, MuPAD issues an error:
f(2/3)
Error: The object '2/3' is incorrect. The type of argument number 1 must be 'DOM_INT'.
Evaluating: f
During a typical procedure call, for example a call to the solve or int function, MuPAD
internally calls many other procedures. Testing argument types for each internal
procedure call is computationally expensive. To provide better performance, MuPAD
reduces the amount of type checks in the running code. By default, the system checks the
4-19
4 Programming Fundamentals
types of arguments only in those procedures that you call interactively. If one procedure
internally calls another procedure, MuPAD does not check types of the arguments of the
internally called procedure. For example, create the following procedure g as a wrapper
for the procedure f:
MuPAD performs type checks only for the arguments of the procedure g, which you call
interactively. It does not perform type checks for the arguments of the procedure f:
g(2/3)
Pref::typeCheck()
To perform type checks in all procedure calls, including internal calls, set the value of
Pref::typeCheck to Always:
Pref::typeCheck(Always):
Now, the system realizes that 2/3 is not a valid argument of the internally called
procedure f:
g(2/3)
Error: The object '2/3' is incorrect. The type of argument number 1 must be 'DOM_INT'.
Evaluating: f
To disable type checks in all procedure calls, including interactive calls, set the value of
Pref::typeCheck to None:
Pref::typeCheck(None):
4-20
Test Arguments
Now, the system does not check argument types in any procedure calls:
g(2/3), f(2/3)
Typically, when you call one MuPAD procedure, that procedure internally calls other
MuPAD procedures. Some of these internal calls are multiple calls to the same procedure
with different sets of arguments. Testing arguments for each internal procedure call can
become computationally expensive. By default, the system uses the following general
principle for testing arguments of a typical MuPAD procedure:
• If you call a procedure interactively, the procedure performs all argument checks.
• If one procedure internally calls another procedure, the second procedure skips
argument checks.
Currently, the procedure p always checks whether the value of its argument belongs to
the interval [-1,1]. To follow the general principle for testing arguments, the procedure
must be able to recognize internal and interactive calls, and skip argument checking
4-21
4 Programming Fundamentals
when a call is internal. For this task, MuPAD provides the testargs function. When you
call testargs inside an interactive procedure call, testargs returns the value TRUE.
For internal procedure calls, testargs returns the value FALSE by default. For example,
rewrite your procedure p as follows:
p := proc(x)
begin
if testargs() then
if abs(x) > 1 then
error("invalid number. Choose a value from the interval [-1,1].");
end_if;
end_if;
arcsin(x)
end_proc:
When you call the procedure p, it checks whether the input argument belongs to the
specified interval:
p(1/2), p(1), p(0)
p(10)
Error: invalid number. Choose a value from the interval [-1,1]. [p]
Now, write the simple wrapper procedure f that calls the procedure p:
f := proc(x) begin p(x) end_proc:
When the wrapper procedure f calls p, the procedure p does not check its arguments
because testargs returns the value FALSE:
f(10)
The testargs function also allows you to switch to the argument checking mode. In
this mode, MuPAD checks arguments of all procedures, regardless of how a procedure is
called. This mode can slow down your computations. Use this mode only for debugging
your code. To switch to the argument checking mode, set the value of testargs to TRUE:
4-22
Test Arguments
testargs(TRUE):
In the argument checking mode, the procedure p checks its argument during interactive
and internal calls:
p(10)
Error: invalid number. Choose a value from the interval [-1,1]. [p]
f(10)
Error: invalid number. Choose a value from the interval [-1,1]. [p]
Always restore testargs to its default value FALSE after you finish debugging:
testargs(FALSE):
4-23
4 Programming Fundamentals
Verify Options
For many standard MuPAD procedures, you can use different options. If a MuPAD
procedure accepts options, it has an embedded mechanism for collecting and verifying
these options. For example, the solve function accepts the Real option. The option
indicates that the solver must return only real solutions and accepts the values TRUE
and FALSE. If an option accepts only TRUE and FALSE values, you can provide the option
name without specifying its value:
solve(x^4 - 1 = 0, x, Real)
If you do not specify the Real option, the solver uses the default option value FALSE, and
returns all complex solutions of the equation:
solve(x^4 - 1 = 0, x)
You can explicitly specify the option-value pair, for example, Real = TRUE or Real =
FALSE:
If you provide an unexpected option (for example, if you spell the option name
incorrectly), MuPAD issues an error indicating the wrong option. If there are several
wrong options, MuPAD indicates the first wrong option:
4-24
Verify Options
Evaluating: solvelib::getOptions
You can embed the same option checking mechanism in your own procedures. For
this task, MuPAD provides the prog::getOptions function, which collects and
verifies options used during a procedure call. When a user calls your procedure,
prog::getOptions scans all arguments and returns a table that contains all expected
options and their values. It also returns a list of all unexpected options.
When you pass arguments to prog::getOptions, always use the following order. The
first argument of prog::getOptions is the number n + 1, where n is the number of
required (non-optional) arguments of the procedure. Then you must provide the list of all
actual arguments followed by the table of all acceptable options and their default values.
To access a sequence of all arguments of a procedure call, including required arguments
and options, use the args function.
The following example demonstrates the procedure that accepts the numeric coefficients
a, b, and c and solves the quadratic equation a x2 + b x + c = 0 using these coefficients.
The procedure solveQuadraticEqn requires the user to provide three numeric values.
Therefore, if you embed prog::getOptions into this procedure, the first parameter
of prog::getOptions must be the number 4. The procedure also accepts the optional
argument PositiveOnly. If the value of PositiveOnly is TRUE, the procedure returns
only positive solutions of the quadratic equation. If the value is FALSE, the procedure
returns all solutions. The following function call to prog::getOptions sets the default
option value PositiveOnly = FALSE:
solveQuadraticEqn(2, 3, -9)
4-25
4 Programming Fundamentals
If you use the PositiveOnly option, the procedure returns only positive solutions:
Now, the procedure solveQuadraticEqn issues an error. The error message indicates
the wrong option:
4-26
Verify Options
4-27
4 Programming Fundamentals
When debugging, you also can use the print and fprint functions for printing
intermediate results produced by your code.
Suppose, you want to create a procedure that computes the Lucas numbers. The Lucas
numbers are a sequence of integers. The recursion formula that defines the nth Lucas
number is similar to the definition of the Fibonacci numbers:
Although MuPAD does not provide a function that computes the Lucas numbers, writing
your own procedure for this task is easy:
lucas:= proc(n:Type::PosInt)
begin
if n = 1 then
1
elif n = 2 then
3
else
lucas(n - 1) + lucas(n - 2)
end_if
end_proc:
The procedure call lucas(n) returns the nth Lucas number. For example, display the
first 10 Lucas numbers:
lucas(n) $ n = 1..10
4-28
Debug MuPAD Code in the Tracing Mode
Suppose you want to trace this procedure. To switch execution of a particular procedure,
domain, method, or function environment to the tracing mode, use the prog::trace
function. For example, to trace the lucas procedure, enter:
prog::trace(lucas):
Now, if you call the lucas procedure, the trace mechanism observes every step of the
procedure call and generates the report for that call:
lucas(5)
enter lucas(5)
enter lucas(4)
enter lucas(3)
enter lucas(2)
computed 3
enter lucas(1)
computed 1
computed 4
enter lucas(2)
computed 3
computed 7
enter lucas(3)
enter lucas(2)
computed 3
enter lucas(1)
computed 1
computed 4
computed 11
The prog::traced() function call returns the names of all currently traced procedures,
domains, methods, and function environments:
prog::traced()
By using different options of the prog::trace function, you can customize generated
reports. Most of these options are independent of a particular procedure call. They affect
all reports generated after you use an option. See the prog::trace help page for more
details.
4-29
4 Programming Fundamentals
If a procedure uses many nested procedure calls, the generated report for that procedure
can be very long. To limit the number of nested procedure calls in a report, use the Depth
option of prog::trace:
prog::trace(Depth = 2):
lucas(5)
enter lucas(5)
enter lucas(4)
computed 7
enter lucas(3)
computed 4
computed 11
The Depth option affects all reports generated for further calls to procedures, domains,
methods, and function environments. If you do not want to use this option for further
calls, set its value to 0. The value 0 indicates that prog::trace must display all nested
calls:
prog::trace(Depth = 0):
To display memory usage in each step of the procedure call, use the Mem option:
prog::trace(Mem):
lucas(5)
4-30
Debug MuPAD Code in the Tracing Mode
To stop using the Mem option for further calls, set its value to FALSE:
prog::trace(Mem = FALSE):
To stop tracing calls to the lucas procedure, use the prog::untrace function:
prog::untrace():
4-31
4 Programming Fundamentals
Display Progress
By default, MuPAD procedures do not show progress information or comments on run
time. For example, create the following procedure that returns the sign of an input
number. (MuPAD provides the standard function sign for this task.)
S := proc(z:Type::Numeric)
begin
if not(testtype(z, Dom::Real)) then
z/abs(z)
elif z > 0 then
1
elif z < 0 then
-1
else
0
end_if
end_proc:
When you execute this procedure, it returns only the final result:
S(10)
Typically, the final result is all that your users want to see. However, if executing a
procedure takes a long time or if users can benefit from the comments on some procedure
steps, you can extend the procedure to include additional information. To embed the
progress information into your procedure, use the print function. For example, modify
the procedure S so it reports its progress:
S := proc(z:Type::Numeric)
begin
print(Unquoted, "Is ".expr2text(z)." a real number?");
if not(testtype(z, Dom::Real)) then
print(Unquoted, expr2text(z)." is a complex number. Computing...
the sign of ".expr2text(z)." as z/|z|");
z/abs(z);
else
print(Unquoted, expr2text(z)." is a real number");
print(Unquoted, "Is ".expr2text(z)." a positive number?");
if z > 0 then
4-32
Display Progress
S(0)
Is 0 a real number?
0 is a real number
Is 0 a positive number?
Is 0 a negative number?
0 is zero.
4-33
4 Programming Fundamentals
Use Assertions
If you rely on correctness of particular statements for your code, then consider including
these statements in the code. Such statements are called assertions. Assertions help you
remember specific conditions under which you expected to execute your code. They can
also help other developers who might need to review or update your code.
MuPAD lets you use checked assertions. If you switch to a special mode of executing your
code, the system evaluates assertions during run time. If an assertion does not evaluate
to TRUE, the system stops code execution and throws an error.
For example, this procedure solves the equation sin(x) + cos(x) = a2. Suppose you get the
parameter a as a result of some computations, and you expect the condition to
be always valid. Relying on this condition, you expect the solutions to be real. Specify this
condition as an assertion by using assert:
f := proc(a)
begin
assert(a^2 <= sqrt(2));
s := solve(sin(x) + cos(x) = a^2)
end:
Assertions are checked only when you run your code in a special mode called the
argument checking mode. Otherwise, the system ignores all assertions. For example, in
this procedure call, MuPAD skips the assertion and returns the following complex result:
f(4/3)
To switch to the argument checking mode, set the value of testargs to TRUE:
4-34
Use Assertions
testargs(TRUE):
Now when you call the procedure f, MuPAD checks the assertion. For a = 4/3, the
assertion evaluates to FALSE, and the procedure execution stops with the error:
f(4/3)
For a = 1, the assertion evaluates to TRUE. The procedure call runs to completion, and
returns the following set of real solutions:
f(1)
The argument checking mode can slow down your computations. Use this mode only
for debugging your code. Always restore testargs to its default value FALSE after you
finish debugging:
testargs(FALSE):
4-35
4 Programming Fundamentals
The error function terminates execution of a current procedure with an error. For
example, the following procedure converts the number of a month to the name of a
month. The procedure checks the type of its argument and accepts only positive integer
numbers. Since there are only 12 months in a year, the procedure must also ensure that
the number does not exceed 12. The error function lets you terminate the procedure call
with an appropriate error message if the number is greater than 12:
monthNumberToName := proc(n:Type::PosInt)
begin
if n > 12 then
error("Invalid number. The number must not exceed 12.")
end_if;
case n
of 1 do return(January)
of 2 do return(February)
of 3 do return(March)
of 4 do return(April)
of 5 do return(May)
of 6 do return(June)
of 7 do return(July)
of 8 do return(August)
of 9 do return(September)
of 10 do return(October)
of 11 do return(November)
of 12 do return(December)
end_case:
end:
If you call monthNumberToName with any integer from 1 to 12, the procedure returns the
name of a month:
monthNumberToName(12)
4-36
Write Error and Warning Messages
If you call monthNumberToName with an integer greater than 12, the procedure
terminates with the specified error:
monthNumberToName(13)
Error: Invalid number. The number must not exceed 12. [monthNumberToName]
Warning messages help you inform your users about potential problems in the algorithm.
These problems are typically minor and do not interfere with the execution of a
procedure. For example, you can warn your users about limited functionality of a
procedure or about implicit assumptions made by a procedure.
The following procedure uses the simplify function to simplify the fraction. The
function implicitly assumes that the variable x is not equal to a. If your procedure uses
the simplify function, you can add the following warning for your users:
simplifyW := proc(a)
begin
warning("Assuming x <> ".expr2text(a));
simplify((x^2 - a^2)/(x - a))
end:
4-37
4 Programming Fundamentals
Handle Errors
Typically, when a MuPAD procedure encounters an error caused by evaluation of an
object, the procedure terminates, the system returns to an interactive level and displays
an error message. If you want to avoid terminating a procedure, use the traperror
function to catch an error.
The traperror function returns the error number instead of the error itself. Later, you
can reproduce the error by using the lasterror function. This function produces the
last error that occurred in the current MuPAD session. If you call lasterror inside
a statement or a procedure, it terminates that statement or procedure. If you want
to display the error message without throwing the error itself, save the error number
returned by traperror, and then retrieve the message by using the getlasterror
function.
For example, the function f throws an error when sin(π k) is equal to zero:
f(1)
Suppose you want to compute f for the values - π ≤ k ≤ π increasing the value of k by
in each step. To call the function f for all required values, create the for loop. When you
try to execute this statement, it terminates as soon as the function f encounters division
by zero for the first time. To avoid terminating the for statement, use the traperror
function to catch the error. To display the text of that error message without interrupting
execution of the for statement, use getlasterror():
4-38
Handle Errors
end_if
end_for
k = -3/4, f = -2^(1/2)
k = -1/2, f = -1
k = -1/4, f = -2^(1/2)
k = 1/4, f = 2^(1/2)
k = 1/2, f = 1
k = 3/4, f = 2^(1/2)
For errors created with the error function, including your custom error messages,
traperror always returns the error code 1028. If an error message with the code 1028
is the last error message that occurred in a MuPAD session, you can retrieve that error
message by using lasterror or getlasterror. For example, create the procedure g
that computes the factorial of any number less than 10. If you pass a number greater
than 10 to this procedure, the procedure reduces the number to 10, and then computes
the factorial:
g := proc(n:Type::PosInt)
local test;
begin
test := proc()
begin
4-39
4 Programming Fundamentals
if n > 10 then
error("The number must not exceed 10.")
end_if
end_proc:
if traperror(test(n)) <> 0 then
g(n - 1)
else
n!
end_if;
end_proc:
Call the procedure with the number 100. During run time, the procedure encounters
the error, but does not terminate. It also does not display the error message. Instead, it
returns the result:
g(100)
To retrieve and display the error message caught during execution of the procedure g,
use the lasterror function:
lasterror()
To display the error code and the text of that message, use the getlasterror function:
g(100):
getlasterror()
4-40
When to Analyze Performance
However, in some cases you might want to measure and investigate your code
performance. This task is also called profiling. For example, profiling is helpful when
you:
MuPAD provides tools to measure the running time of a particular code snippet or the
whole MuPAD session. The system also provides the profiling tool to help you find and
eliminate performance bottlenecks in your code.
4-41
4 Programming Fundamentals
Measure Time
In this section...
“Calls to MuPAD Processes” on page 4-42
“Calls to External Processes” on page 4-44
The following example demonstrates different algorithms implemented for the same
task. The task is to check whether each integer from 1 to 1000 appears in a 1000×1000
matrix of random integers. To create a 1000×1000 matrix of random integers, use
linalg::randomMatrix:
matrixSize := 1000:
M := linalg::randomMatrix(matrixSize, matrixSize, Dom::Integer):
The direct approach is to write a procedure that checks every element of a matrix
proceeding row by row and column by column:
f := proc(M, n, x)
begin
for j from 1 to n do
for k from 1 to n do
if M[j, k] = x then
return(TRUE)
end_if
end_for
end_for;
return(FALSE)
end_proc:
Call the procedure f 1000 times to check if each number from 1 to 1000 appears in that
matrix:
4-42
Measure Time
g := proc()
begin
f(M, matrixSize, i) $ i = 1..1000
end_proc:
This algorithm is very inefficient for the specified task. The function f performs 104
computation steps to find out that an integer does not occur in the matrix M. Since you
call the function f 1000 times, executing this algorithm takes a long time:
time(g())
62435.902
In this example, the bottleneck of the chosen approach is obviously the algorithm that
accesses each matrix element. To accelerate the computation, rewrite the procedure f
using the bisection method. Before using this method, convert the matrix M to a list and
sort the list. Then select the first and last elements of the sorted list as initial points.
Each step in this algorithm divides the list of elements at the midpoint:
f := proc(M, n, x)
begin
if (M[1] - x)*(M[n] - x) > 0 then
return(FALSE)
elif (M[1] - x)*(M[n] - x) = 0 then
return(TRUE);
else
a := 1: b := n:
while (b - a > 1) do
if is(b - a, Type::Odd) then
c := a + (b - a + 1)/2
else
c := a + (b - a)/2
end_if;
if M[c] - x = 0 then
return(TRUE)
elif (M[a] - x)*(M[c] - x) < 0 then
b := c:
else
a := c:
end_if;
end_while;
end_if;
return(FALSE)
end_proc:
4-43
4 Programming Fundamentals
Use the op function to access all elements of the matrix M. This function returns a
sequence of elements. Use brackets to convert this sequence to a list. Then use the sort
function to sort the list in ascending order. Finally, call the procedure f for each integer
from 1 to 1000:
g := proc()
local M1;
begin
M1 := sort([op(M)]):
f(M1, matrixSize^2, i) $ i = 1..1000
end_proc:
Using the bisection method instead of accessing each matrix element significantly
improves the performance of the example:
time(g())
3724.233
Typically, the best approach is to use the appropriate MuPAD functions whenever
possible. For example, to improve performance further, rewrite the code using the
MuPAD function has. Also, converting a matrix to a set can reduce the number of
elements. (MuPAD removes duplicate elements of a set.) In addition to speed up, this
approach makes your code significantly shorter and easier to read:
g := proc()
local M1;
begin
M1 := {op(M)}:
has(M1, i) $ i = 1..1000
end_proc:
In this case, execution time is even shorter than for the code that implements the
bisectional method:
time(g())
1508.094
4-44
Measure Time
that code, including calls to external processes. To measure the total time, use the rtime
function instead of time. For example, the function call rtime() returns the elapsed
time of the current MuPAD session. This time includes idle time of the current session:
t := rtime():
print(Unquoted, "This session runtime is ".stringlib::formatTime(t))
When measuring code performance using rtime, avoid running other processes in the
background. Also ensure that enough memory is available. The rtime function counts
the total time, including idle time during which some other process uses the CPU or your
computer swaps data between different types of memory.
4-45
4 Programming Fundamentals
The prog::profile function evaluates the code that you want to profile, and returns
the profiling report. The report contains:
• A table showing the time your code spent on each function (total and averaged per one
call), number of calls to that function, children of that function, and so on. Information
for the most heavily used functions appear on the top of the table. The first row in this
table represents the total execution time.
• A dependence graph showing a function itself, the functions it calls (children), and the
functions that call this function. The graph also shows the timing and number of calls
for each function.
Note: By default, prog::profile does not measure performance of single calls to kernel
functions.
Suppose you want to write a procedure that checks whether each integer from 1 to 1000
appears in a 1000×1000 matrix of random integers. The direct approach is to write a
procedure that checks every element of a matrix proceeding row by row and column by
column:
f := proc(M, n, x)
begin
for j from 1 to n do
for k from 1 to n do
if M[j, k] = x then
return(TRUE)
end_if
4-46
Profile Your Code
end_for
end_for;
return(FALSE)
end_proc:
Then call the procedure f 1000 times to check if each number from 1 to 1000 appears in
that matrix:
g := proc()
begin
f(M, matrixSize, i) $ i = 1..1000
end_proc:
Measuring the time needed to run the procedure g shows that the procedure requires
optimization. Although the performance bottleneck in this procedure is obvious, it is not
always easy to identify performance bottlenecks in more complicated procedures. The
time function does not indicate where the procedure spends most of its execution time:
time(g())
62023.876
To obtain the complete profiling report that shows timings for all inner function calls, use
prog::profile:
prog::profile(g()):
| | time self
| | |
| time children
| | | | | calls/normal exit
4-47
4 Programming Fundamentals
| | | |
| | calls/remember exit
| | | | | | | calls/errors
| | | |
| | | | [index] function name
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
45.9 . 50467.2 .
27149.6 3019825 . . [1] (Dom::Matrix(Dom::Integer))::_index_intern
19.7 . 21689.3
. 5460.3 3019825 . . [2] Dom::Integer::coerce
16.7
. 18373.1 . 77616.9 3019825 . . [3] (Dom::Matrix(Dom::Integer)):
5.0
. 5460.3 . . 3019825 . . [5] Dom::Integer::convert
.
8.0 8.0 109974.9 109974.9 1 . . [6] g
---------------------------------------------------------------------------------------
index
%time self children called [index] name
4-48
Profile Your Code
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
4-49
4 Programming Fundamentals
---------------------------------------------------------------------------------------
5460.346 0 3019825
[2] Dom::Integer::coerce
---------------------------------------------------------------------------------------
13984.84
95990.02 1000 [4] f
---------------------------------------------------------------------------------------
Top rows of the profiling report indicate that the procedure spends most of its time
accessing each matrix element. To improve performance, rewrite the procedure so that it
can access fewer elements of a matrix in each call to the procedure f. For example, use
the algorithm based on the bisection method:
f := proc(M, n, x)
begin
if (M[1] - x)*(M[n] - x) > 0 then
return(FALSE)
elif (M[1] - x)*(M[n] - x) = 0 then
return(TRUE);
else
a := 1: b := n:
while (b - a > 1) do
if is(b - a, Type::Odd) then
c := a + (b - a + 1)/2
else
c := a + (b - a)/2
end_if;
if M[c] - x = 0 then
return(TRUE)
elif (M[a] - x)*(M[c] - x) < 0 then
b := c:
4-50
Profile Your Code
else
a := c:
end_if;
end_while;
end_if;
return(FALSE)
end_proc:
Before calling the procedure f, you must convert the matrix M to a list and sort that list.
Sorting the list that contains 104 entries is an expensive operation. Depending on the
number of calls to the procedure f, this operation can potentially eliminate the increase
in performance that you gain by improving the procedure f itself:
g := proc()
local M1;
begin
M1 := sort([op(M)]):
f(M1, matrixSize^2, i) $ i = 1..1000
end_proc:
For these particular matrix size and number of calls to f, implementing the bisectional
algorithm is still efficient despite the time required to sort the list:
time(g())
3840.24
The profiling report shows that the procedure spends most of the time executing the op
and g function calls. This is because implementation of the bisection algorithm added
new expensive operations in g (conversion of a matrix to a list and then sorting the list).
The profiling report generated for the procedure call g() is very long. This example
shows only the top of the report:
prog::profile(g()):
| | time self
| | |
4-51
4 Programming Fundamentals
| time children
|
| | | | calls/normal exit
| | | | |
| calls/remember exit
|
| | | | | | calls/errors
| | | | |
| | | [index] function name
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
. . . . .
2 . . [7] DomainConstructor::hasProp
. . . . .
. 9981 . [8] is
---------------------------------------------------------------------------------------
4-52
Profile Your Code
The recommended approach for improving performance of your code is to use the MuPAD
functions when possible. For example, MuPAD provides the has function for checking
whether one MuPAD object contains another MuPAD object. Rewrite your code using the
has function and combining the procedures f and g:
g := proc()
local M1;
begin
M1 := {op(M)}:
has(M1, i) $ i = 1..1000
end_proc:
This procedure also converts the matrix M to a set. Converting a matrix to a set can
reduce the number of elements. (MuPAD removes duplicate elements of a set.)
The execution time for the procedure call g() is the shortest among the three
implementations:
time(g())
1520.095
The profiling report shows that the procedure spends most of its execution time accessing
the 1000×1000 matrix of random integers and converting it to a set. This example shows
only the top of the profiling report:
prog::profile(g()):
| | time self
| | |
| time children
|
| | | | calls/normal exit
| | | | |
| calls/remember exit
4-53
4 Programming Fundamentals
|
| | | | | | calls/errors
| | | | |
| | | [index] function name
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
. . . . . 2 .
. [5] Dom::Integer::hasProp
. . . . . 2 .
. [6] DomainConstructor::hasProp
---------------------------------------------------------------------------------------
4-54
Techniques for Improving Performance
• Use built-in MuPAD data types and functions when possible. Typically, these
functions are optimized to handle your computation tasks faster and smoother.
• Set assumptions on parameters when possible. Use the assumptions of variables
sparingly or avoid them completely. For details about how assumptions affect
performance, see When to Use Assumptions.
• Call special solvers directly instead of using general solvers. If you can determine the
type of an equation or system that you want to solve, calling the special solver for that
equation or system type is more efficient. See Choosing a Solver.
• Call numeric solvers directly if you know that a particular problem cannot be solved
symbolically. This technique has a significant disadvantage: for nonpolynomial
equations numeric solvers return only the first solution that they find.
• Try using options. Many MuPAD functions accept options that let the system reduce
computation efforts. For information about the options of a particular MuPAD
function, see the “Options” section of the function help page.
• Limit complexity of the expressions that you use.
• Use shorter data structures when possible. For example, converting a sequence with
106 entries to a list takes longer than converting 1000 sequences with 1000 entries
each.
• Avoid creating large symbolic matrices and dense matrices when possible. For details
about improving performance when working with matrices, see Using Sparse and
Dense Matrices.
• Avoid using for loops to create a sequence, a flat list, a string and similar data
structures by appending new entries. Instead, use the sequence generator $.
• Use for loops as outer loops when creating deep nested structures. Use the sequence
generator $ for inner loops.
4-55
4 Programming Fundamentals
• Use the remember mechanism if you call a procedure with the same arguments more
than once. The remember mechanism lets you avoid unnecessary reevaluations. See
Remember Mechanism. At the same time, avoid using the remember mechanism for
nonrecurring procedure calls, especially if the arguments are numerical.
• Avoid storing lots of data in the history table. If you suspect that the history table
uses a significant amount of memory, clear the history table or reset the engine. For
information about the history table, see History Mechanism.
• Avoid running large background processes, including additional MuPAD sessions, at
the same time as you execute code in MuPAD.
4-56
Display Memory Usage
The simplest tool for observing the memory usage is the status bar. You can find the
status bar at the bottom of a MuPAD notebook. If you do not see the status bar, select
View > Status Bar. The far left end of the status bar displays the current engine
state, including memory and time used during the most recent computations. While a
computation is still running, the status bar information keeps changing.
If the engine is not connected to your notebook, the status bar displays Not Connected.
For more information about the status bar, see Viewing Status Information.
Note: When you perform computations in several MuPAD notebooks, each notebook
starts its own engine. In this case, watch for the total amount of memory used by all
MuPAD engines (the mupkern.exe processes).
4-57
4 Programming Fundamentals
usage, reserved memory, and evaluation time. You can control the frequency with which
MuPAD prints such messages.
To set the frequency of these periodic messages, use the Pref::report function.
By default, the value of Pref::report is 0; MuPAD does not print periodic status
messages. If you increase the value to 1, MuPAD prints status messages approximately
every hour. (The exact frequency depends on your machine.) The maximum value
accepted by Pref::report is 9.
Suppose you want to generate and sort the list of 10,000,000 random integer numbers.
These operations take a long time because of the huge number of elements. If you set the
value of Pref::report to 4, MuPAD displays a few status messages while executing
these operations:
Pref::report(4):
sort([random() $ i = 1..10^7]):
If you increase the value of Pref::report to 6, MuPAD prints the status messages
more frequently:
Pref::report(6):
sort([random() $ i = 1..10^7]):
Every time you execute this example, MuPAD adds a new list of 107 random numbers
and stores that list in the history table. By default, the history table contains up
to 20 elements. While this list remains in the history table, MuPAD cannot release
the memory needed to store 107 integers. To release this memory, use one of these
alternatives:
4-58
Display Memory Usage
• Continue computations waiting until MuPAD writes 20 new elements to the history
table. Performing computations with a reduced amount of available memory can be
very slow.
• Terminate the MuPAD engine connected to the notebook by selecting
Notebook > Disconnect. The new engine starts when you evaluate any command in
the notebook.
• Clear the history table by setting the value of variable HISTORY to 0. This variable
specifies the maximum number of elements in the history table. To restore the default
value of HISTORY, enter delete HISTORY:
HISTORY := 0:
delete HISTORY:
HISTORY
For more information about the history mechanism in MuPAD, see History Mechanism.
Pref::report(NIL):
For example, create the recursive procedure juggler that computes the Juggler number
sequence for any initial positive integer n:
juggler := proc(n:Type::PosInt)
begin
J := append(J, n);
if n = 1 then
return(J)
end_if:
if testtype(n, Type::Even) then
juggler(floor(n^(1/2)))
else
juggler(floor(n^(3/2)))
4-59
4 Programming Fundamentals
end_if
end_proc:
Suppose you want to see the memory usage report for every call to this procedure.
First call the prog::trace function with the Mem option. Then switch execution of the
juggler procedure to the tracing mode:
prog::trace(Mem):
prog::trace(juggler)
Now when you call the juggler procedure, the tracing report shows the memory usage
for each call to juggler:
J := []:
juggler(7)
enter
juggler(7) [mem: 5338408] enter juggler(18) [mem: 5373600] enter
juggler(4) [mem: 5374080] enter juggler(2) [mem: 5374584]
enter juggler(1) [mem: 5375064] computed [7, 18, 4,
2, 1] [mem: 5375032] computed [7, 18, 4, 2, 1] [mem: 5374648]
computed [7, 18, 4, 2, 1] [mem: 5374264] computed [7, 18, 4,
2, 1] [mem: 5373880] computed [7, 18, 4, 2, 1] [mem: 5373524]
The Mem option is independent of the traced procedure. Now if you use prog::trace to
trace any other procedure, prog::trace displays memory usage in every step of that
procedure. Remove this global option for further computations:
prog::trace(Mem = FALSE)
prog::untrace(juggler):
4-60
Remember Mechanism
Remember Mechanism
In this section...
“Why Use the Remember Mechanism” on page 4-61
“Remember Results Without Context” on page 4-63
“Remember Results and Context” on page 4-64
“Clear Remember Tables” on page 4-65
“Potential Problems Related to the Remember Mechanism” on page 4-67
4-61
4 Programming Fundamentals
3
else
lucas(n - 1) + lucas(n - 2)
end_if
end_proc:
However, if the value n is large, computing the nth Lucas number can be very slow. The
number of required procedure calls is exponential. Often, the procedure calls itself with
the same arguments, and it reevaluates the result in every call:
time(lucas(35))
Using the remember mechanism eliminates these reevaluations. To enable the remember
mechanism for a particular procedure, use the prog::remember function. This function
returns a modified copy of a procedure that stores results of previous calls in the
remember table:
lucas := prog::remember(lucas):
When you call this procedure, MuPAD accesses the remember table. If the system finds
the required entry in the remember table, it returns remembered results immediately.
Now, MuPAD computes the 35th and even the 100th Lucas number almost instantly:
time(lucas(35)), time(lucas(100))
Alternatively, you can enable the remember mechanism for a particular procedure by
using the option remember for that procedure. For example, use the option remember to
enable the remember mechanism for the procedure lucas:
lucas:= proc(n:Type::PosInt)
option remember;
begin
if n = 1 then
1
elif n = 2 then
3
else
lucas(n - 1) + lucas(n - 2)
4-62
Remember Mechanism
end_if
end_proc:
delete lucas:
f := (x)-> 1.0/x:
f := prog::remember(f):
The default number of significant digits for floating-point numbers is 10. Use the
function f to compute the reciprocal of 3. The system displays the result with the 10-
digits accuracy:
f(3)
Now increase the number of digits to 50. Then call the function f with the argument 3
again. By default, MuPAD does not realize that you increased the required accuracy. The
system accesses the remember table, finds the entry that corresponds to the argument
3, and returns the result previously computed for that argument. Since MuPAD must
display the output with 50 digits, the last digits in the displayed result are incorrect:
DIGITS := 50:
f(3)
For further computations, restore the default value of DIGITS and delete f:
4-63
4 Programming Fundamentals
delete DIGITS, f
Note: The option remember does not let you specify the dependency function. If results
of a procedure depend on the context information, use the prog::remember function for
that procedure.
In this example, the dependency function is a list that checks both the properties of input
arguments and the value of DIGITS:
f := (x)-> 1.0/x:
f := prog::remember(f, () -> [property::depends(args()), DIGITS]):
The default number of significant digits for floating-point numbers is 10. Use the
function f to compute the reciprocal of 3. The system displays the result with the 10-
digits accuracy:
f(3)
If you set the number of digits to 50, and then call the function f with the same
argument 3, prog::remember realizes that the number of digits has changed. Instead of
returning the previous result stored in the remember table, the system reevaluates the
result and updates the remember table:
4-64
Remember Mechanism
DIGITS := 50:
f(3)
For further computations, restore the default value of DIGITS and delete f:
delete DIGITS, f
Create the following procedure f as a wrapper for the MuPAD heaviside function. Use
prog::remember to enable the remember mechanism for the procedure f:
f := proc(x)
begin
heaviside(x)
end:
f := prog::remember(f):
Now compute the Heaviside function for the values -10, 0, and 10. MuPAD uses the value
heaviside(0)=1/2:
You can define a different value for heaviside(0). First, use the unprotect
function to be able to overwrite the value of heaviside. Then, assign the new value to
heaviside(0):
unprotect(heaviside):
heaviside(0):= 0:
Despite the new value heaviside(0) = 0, the wrapper procedure f returns the old
value 1/2:
4-65
4 Programming Fundamentals
f(0)
The result of the procedure call f(0) does not change because the system does not
reevaluate this result. It finds the result in the remember table of the procedure f and
returns that result. To display the content of the remember table, call the wrapper
procedure f with the Remember option as a first argument and the Print option as a
second argument. The value 106 in the second column is the value of MAXEFFORT used
during computations.
f(Remember, Print)
To force reevaluation of the procedure calls of f, clear the remember table of that
procedure. To clear the remember table, call f with the Remember option as a first
argument and the Clear option as a second argument:
f(Remember, Clear):
f(0)
If you use the option remember, you also can clear the remember table and force
reevaluation. For example, rewrite the procedure f as follows:
f := proc(x)
option remember;
begin
heaviside(x)
end:
4-66
Remember Mechanism
f(0)
heaviside(0):= 1/2:
To clear a remember table created by the option remember, use the forget function:
forget(f):
f(0)
Use the protect function with the ProtectLevelError option to prevent further
changes to heaviside. Also, delete the procedure f:
protect(heaviside, ProtectLevelError):
delete f
• Remember tables are efficient only if the access time of the remember table is
significantly less than the time needed to evaluate the result. If a remember table
is very large, evaluation can be computationally cheaper than accessing the result
stored in the remember table.
• Storing large remember tables requires a large amount of memory. Especially,
remember tables created with the option remember can grow very large, and
significantly reduce available memory. The number of entries in remember tables
created by prog::remember is limited. When the number of entries in a remember
table created by prog::remember reaches the maximum number, the system
removes a group of older entries.
• Using prog::remember or the option remember for nonrecurring procedure calls can
significantly decrease code performance. Avoid using the remember mechanism for
nonrecurring procedure calls, especially if the arguments are numerical.
4-67
4 Programming Fundamentals
• If you change the properties of input arguments or modify the variables DIGITS or
ORDER, the remember mechanism ignores these changes by default. See Remembering
Results Without Context.
• In some cases you must clear the remember table of a procedure to enforce
reevaluation and avoid incorrect results. For example, clearing the remember table
can be necessary when a procedure changes global variables or if global variables
affect the results of a procedure. See Clearing Remember Tables.
• Many predefined MuPAD functions have special values stored in their remember
tables. Therefore, clearing the remember tables of predefined MuPAD functions is
not recommended. Note that the forget function does not error when you call it for a
predefined MuPAD function.
4-68
History Mechanism
History Mechanism
In this section...
“Access the History Table” on page 4-69
“Specify Maximum Number of Entries” on page 4-72
“Clear the History Table” on page 4-73
To access the entries of the history table, use the last and history functions. The last
function returns previously computed results without the command that generated the
results. The history function returns the previously computed results along with the
commands that generated those results.
For the last function, MuPAD also provides the shortcut %. The function call last(n)
(or % n) returns the nth entry of the history table. The last function counts the entries
of the history table from the end of the table. Thus, when you use the last function, the
most recent result is the first entry of the history table.
For example, compute the factorials of the numbers 10, 20, and 30:
4-69
4 Programming Fundamentals
To access the computed factorial of 30, use the function call last(3) or the shorter call
%3:
last(3)
Note: When you call the last or history function, MuPAD adds the result of that call
to the history table.
Calling the last function or its shortcut % inserts a new entry in the history table. Thus,
the history table now contains the results of the following evaluations: 10!, 20!, 30!,
and %3 (which in this example is equal to 10!). If you call the last function with the
argument 3 again, the system displays the result of evaluation of 20!:
last(3)
To access the most recent entry of the history table, you can use the shortcut % without
parameters. For example, solve the following equation, and then simplify the result. Note
that using % lets you avoid assigning the result of a solution to an identifier:
simplify(%)
The last function does not evaluate results. The last function also returns the results
for which you used colons to suppress the outputs:
4-70
History Mechanism
hold(2 + 2):
%
The history function displays both the result and the command that produced that
result. This function counts the entries of the history table from the first result obtained
in the current MuPAD session. Thus, when you use the history function, the most
recent result is the last entry of the history table. In this section, the first entry of the
history table is the computation of the factorial of 10:
history(1)
To find the current number of entries in the history table, call the history function
without an argument:
history()
You can use the history function to access the most recent computation and its result:
a := 2:
history(history())
For the following statements, the history mechanism depends on whether you call the
statement interactively or within a procedure:
These statements are called compound statements. At the interactive level, MuPAD
stores compound statements as one unit. In procedures, MuPAD stores the statements
4-71
4 Programming Fundamentals
found within a compound statement in a separate history table of the procedure. In this
case, the system does not store the compound statement itself.
To change the maximum number of entries in the history table for the current MuPAD
session, assign the new value to HISTORY:
HISTORY := 2:
Now MuPAD stores only the two most recent commands and their results in the history
table:
a := 1: b := 2: c := 3:
%1, %2;
%3
Note: Within a procedure, the maximum number of entries in the local history table of
the procedure is always 3, independent of the value of HISTORY.
For further computations, restore the default maximum number entries in the history
table:
delete HISTORY:
4-72
History Mechanism
HISTORY
For example, set the value of HISTORY to 0 before creating a sequence of 1,000,000
random numbers:
HISTORY := 0:
random() $ i = 1..10^6:
For further computations, restore the default maximum number entries in the history
table:
delete HISTORY:
HISTORY
If the history table already contains a memory-consuming result, to release the memory
you also can clear the history table by setting the value of HISTORY to 0. Alternatively,
you can wait until the MuPAD fills the history table with new entries. Also, you can
select Notebook > Disconnect to restart the MuPAD engine.
4-73
4 Programming Fundamentals
Suppose you create the procedure that accepts two numeric arguments, a and b,
compares them, and returns the larger number:
f := proc(a:Type::Numeric, b:Type::Numeric)
begin
if a = b or a > b then
return(a)
else
return(b)
end_if
end_proc:
The type Type::Numeric includes integers, rationals, floating-point numbers, and also
complex numbers. Therefore, any complex numbers are valid arguments of the procedure
f. The procedure f has a flaw in its design because you cannot compare two complex
numbers. If you call this procedure for two different complex numbers, the procedure
call results in an error. Nevertheless, the procedure works if you call it for equal complex
numbers:
f(I, I)
Suppose you decide to keep the procedure f as it is. It works for equal complex
arguments. The error only occurs when the complex arguments are not equal. Later,
you or somebody else forgets about the issue with complex numbers, but sees that the
procedure can be improved as follows:
f := proc(a:Type::Numeric, b:Type::Numeric)
begin
4-74
Why Test Your Code
if a >= b then
return(a)
else
return(b)
end_if
end_proc:
This code looks shorter, and takes advantage of the >= operator. However, if some users
relied on the procedure f to recognize equal complex numbers, their code breaks when
they use the updated version:
f(I, I)
If you do not create and use a test script for the procedure f, you might never realize
that the procedure stopped working for the particular choices of arguments. Even if you
tested this choice of arguments before, you might forget to test it for the updated version.
Writing a test script and running it every time when you (or somebody else) update your
code helps to avoid unexpected loss in functionality.
4-75
4 Programming Fundamentals
f := proc(a:Type::Numeric, b:Type::Numeric)
begin
if a = b or a > b then
return(a)
else
return(b)
end_if
end_proc:
To test the procedure, use the prog::test function. If the test does not reveal any
problems, prog::test returns the void object null() and does not print anything:
prog::test(f(I, I), I)
If the procedure call tested by prog::test errors or if actual results differ from expected
results, prog::test prints information about the test execution. For example, if your
test compares two different complex numbers, prog::test returns the following
message:
prog::test(f(2*I, I), I)
Error in test 2
Input: f(2*I, I)
Expected: I
Near line: 1
If the error is expected, you can rewrite the test using the TrapError option:
When you call prog::test, MuPAD evaluates actual and expected results before
comparing them:
4-76
Write Single Tests
Error in test 4
Input: f(x^2 | x = 2, 5)
Expected: 4
Got: 5
Near line: 1
Evaluation of actual and expected results can take a long time. To avoid long evaluations,
the prog::test function lets you specify the time limit for evaluation of the test.
To limit the evaluation time for a particular test, use the Timeout option of the
prog::test function. For example, set the time limit to 2 seconds:
prog::test(f([i! $ i = 1..1000000], [i! $ i = 1..1000000]),
[i! $ i = 1..1000000], Timeout = 2)
Expected:
FAIL
Timeout:
2.0 (5.106*prog::ntime())
In this example, the time limit measurement depends on your hardware configuration.
The test report also shows the hardware-independent time in terms of the prog::ntime
function.
By default, prog::test tests the strict equality between actual and expected results.
Testing equality of floating-point values can be confusing when the display precision
differs from the internal precision. In this case, different floating-point numbers can
look identical. Thus, with the default values of DIGITS and Pref::outputDigits, the
floating-point approximation of 1/3 and the number 0.3333333333 look identical:
4-77
4 Programming Fundamentals
prog::test(float(1/3), 0.3333333333)
Error in test 5
Input: float(1/3)
Expected: 0.3333333333
Got: 0.3333333333
Near line: 1
Internally, MuPAD uses more than 10 digits to approximate 1/3 with the floating-
point number. The system adds guard digits for increased precision. To see how
many guard digits the system uses, increase the number of output digits using the
Pref::outputDigits function. Then, test the equality of the numbers again:
Pref::outputDigits(20):
prog::test(float(1/3), 0.3333333333)
Error in test 6
Input: float(1/3)
Expected: 0.3333333333
Got: 0.33333333333333333304
Near line: 2
When you test equality of floating-point numbers, it can be helpful to test the
approximate equality. The approximate equality operator in MuPAD is ~=. The
corresponding function is _approx. The prog::test function lets you choose the
method for comparing actual and expected results. For example, 1/3 is approximately
equal to 0.3333333333 within the default 10-digits precision:
prog::test(float(1/3), 0.3333333333, Method= `~=`)
Also, using the Method option lets you specify more than one acceptable solution. For
example, if you randomly pick one solution of the following equation, you can get any of
its four valid solutions:
i := random(1..4):
prog::test(solve(x^4 - 16 = 0, x)[i()],
4-78
Write Single Tests
4-79
4 Programming Fundamentals
Suppose you will likely update this procedure in the future. To ensure that the procedure
works as expected after all possible updates, write the test script and execute it after
any update. The test script in MuPAD includes a set of single tests created by the
prog::test function. See Writing Single Tests for more information.
Each test script starts with the prog::testinit function and ends with the
prog::testexit function. To specify the name of the tested procedure, use
print(Unquoted, "testname") after prog::testinit. This name does not affect
the tested procedure itself. It only appears in the test reports generated by your test
script.
To test the procedure f for different choices of parameters, write the following test
script and save it to the test-f.tst file. The test does not find any unexpected results
or errors. After MuPAD executes the test script, it generates the test report. The test
script does not require any particular file extension. You can use any file extension
that you like. The test report shows the number of executes tests, the number of errors
encountered while executing the test script, and the time and memory used to execute
the test script:
//test-f.tst
prog::testinit("f");
print(Unquoted, "function f that compares two numbers")
4-80
Write Test Scripts
prog::testexit(f)
Info:
11 tests, 0 errors, runtime factor 0.0 (nothing expected)
If you change the original procedure f, run the test script to catch any unexpected
results:
f := proc(a:Type::Numeric, b:Type::Numeric)
begin
if a >= b then
return(a)
else
return(b)
end_if
end_proc:
You do not need to copy the test script to the notebook. Instead, you can execute the test
script that you saved to a file without opening the file. To execute a test script:
1 Select Notebook > Read Commands to open the Read Commands dialog box.
2 Change the file filter to show MuPAD test files or all files.
4-81
4 Programming Fundamentals
3 Navigate to the test file that contains the script and click OK.
Alternatively, use the READPATH variable to specify the path to the folder that contains
the file. Then use the read function to find and execute the test file. For example, if you
saved the test file test-f.tst in the folder C:/MuPADfiles/TestScripts, use the
following commands:
READPATH := "C:/MuPADfiles/TestScripts":
read("test-f.tst")
Error
in test function f that compares two numbers 7
Expected:
2*I
Near line: 9
Input: f(2 + I, 2 + I)
Expected: 2 + I
Got:
TrapError = [1003, "Error: Can't evaluate to boolean [_leequal];\r\n
Evaluating: f"]
Near line: 10
Info: 11
tests, 2 errors, runtime factor 0.0 (nothing expected)
Although the change seems reasonable and safe, the test report shows that the procedure
does not work for equal complex numbers anymore. Instead, the procedure throws an
4-82
Write Test Scripts
error. If you do not test the code, you can miss this change in procedure behavior. If this
behavior is expected, correct the test script. Otherwise, correct the procedure.
4-83
4 Programming Fundamentals
Code Verification
Even if your code executes without errors, and all your tests run without failures, the
code can still have some flaws. For example, it can:
To ensure that your code does not introduce such flaws, use the prog::check
function to verify it. Use this function to check your procedures, domains, and function
environments. Suppose you wrote the following procedure:
f := proc(x, n)
local a, b, c;
begin
a := m; b := a;
if x > 0 then
x := level(b, 2)
else
x := -level(b, 2)
end_if;
end:
f(42, 24)
prog::check(f, 3)
4-84
Code Verification
Warnings: 3 [f]
For the list of all available options, see the prog::check help page.
4-85
4 Programming Fundamentals
If you create a new MuPAD procedure, it is recommended to protect the procedure and
all its options, especially if you often use that procedure. For example, MuPAD does not
provide a function for computing Lucas numbers. You can write your own procedure for
computing Lucas numbers, and then protect the procedure name.
The Lucas numbers are a sequence of integers. The recursion formula that defines the
nth Lucas number is similar to the definition of the Fibonacci numbers:
Create the following procedure that computes the nth Lucas number:
lucas:= proc(n:Type::PosInt)
option remember;
begin
if n = 1 then
1
elif n = 2 then
3
else
lucas(n - 1) + lucas(n - 2)
end_if
end_proc:
lucas(i) $ i = 1..5
Now protect the procedure name, lucas, using protect with the ProtectLevelError
option:
protect(lucas, ProtectLevelError):
ProtectLevelError lets you set full protection for the identifier. Now, trying to assign
any value to lucas results in error:
4-86
Protect Function and Option Names
lucas := 0
Alternatively, you can use the ProtectLevelWarning option. In this case, you can still
assign a value to the protected identifier, but a warning appears, for example:
protect(lucas, ProtectLevelWarning):
You can assign any value to lucas now, but such assignment triggers a warning:
lucas := 0
unprotect(lucas):
4-87
4 Programming Fundamentals
Data Collection
In this section...
“Parallel Collection” on page 4-88
“Fixed-Length Collection” on page 4-90
“Known-Maximum-Length Collection” on page 4-91
“Unknown-Maximum-Length Collection” on page 4-92
Parallel Collection
Suppose the data that you want to collect is generated element-by-element and you know
in advance how many elements will be generated. The intuitive approach for collecting
such data is to create an empty list and append each new element to the end of the list.
For example, this procedure uses this approach to collect random integers generated by
random:
col :=
proc(n)
local L, i;
begin
L := [];
for i from 1 to n do
L := L.[random()];
end_for;
end:
col(5)
To estimate the performance of this approach, use the procedure col to generate a list of
50,000 random numbers:
time(col(50000))
4-88
Data Collection
Now, check how much time the procedure actually spends generating random numbers:
time(random() $ i = 1..50000)
Thus, the procedure spends most of the time appending the newly generated numbers to
a list. In MuPAD, appending a new entry to a list of n entries takes time proportional to
n. Therefore, run time of col(n) is proportional to n2. You can visualize this dependency
by plotting the times that col(n) spends when creating lists of 1 to 50,000 entries:
plotfunc2d(n -> time(col(n)), n = 1..50000,
Mesh = 20, AdaptiveMesh = 0)
When appending a new entry to a list, MuPAD allocates space for the new, longer
list. Then it copies all entries of the old list plus a new entry to this new list. The
faster approach is to create the entire list at once, without adding each new entry
separately. This approach is called parallel collection because you create and collect data
simultaneously. Use the sequence operator $ to implement this approach:
4-89
4 Programming Fundamentals
col := proc(n)
local i;
begin
[random() $ i = 1..n];
end:
time(col(50000))
Fixed-Length Collection
Suppose you know how many elements you will generate, but you cannot generate them
all at once. In this case, the best strategy is to create a list of the required length filling it
with some constant, such as 0 or NIL. Then you can replace any entry of this list with the
generated value. In this case, you do not need to generate elements in the order in which
you want them to appear in the list.
For example, use this procedure to generate the list of the first n Lucas numbers. The
procedure creates a list of n entries, where each entry is 0. Then it assigns the values to
the first two entries. To replace all other entries of the list with the Lucas numbers, the
procedure uses the for loop:
lucas :=
proc(n)
local L, i;
begin
L := [0 $ n];
L[1] := 1;
L[2] := 3;
for i from 3 to n do
L[i] := L[i-1] + L[i-2];
end_for;
L
end:
time(lucas(10000))
4-90
Data Collection
If you use the procedure that creates an empty list and appends each generated Lucas
number to this list, then creating a list of 10,000 Lucas numbers takes much longer:
lucas :=
proc(n)
local L, i;
begin
L := [];
L :=L.[1];
L := L.[3];
for i from 3 to n do
L := L.[L[i-1] + L[i-2]];
end_for;
L
end:
time(lucas(10000))
Known-Maximum-Length Collection
If you cannot predict the number of elements that you will generate, but have a
reasonable upper limit on this number, use this strategy:
1 Create a list with the number of entries equal to or greater than the upper limit.
2 Generate the data and populate the list.
3 Discard the unused part of the list.
For example, use the following procedure to create a list. The entries of this list are
modular squares of a number a (a2 mod n). You cannot predict the number of entries in
the resulting list because it depends on the parameters a and n. Nevertheless, you can
see that in this case the number of entries in the list cannot exceed n:
modPowers :=
proc(a, n)
local L, i;
begin
4-91
4 Programming Fundamentals
L := [0 $ n];
L[1] := a;
L[2] := a^2 mod n;
i := 2;
while L[i] <> a do
L[i + 1] := a*L[i] mod n;
i := i + 1;
end_while;
L := L[1..i - 1];
end:
When you call modPowers for a = 3 and a = 2, it creates two lists of different lengths:
modPowers(3, 14);
modPowers(2, 14)
Unknown-Maximum-Length Collection
Often, you cannot predict the number of elements and cannot estimate the upper
limit on this number before you start generating actual data. One way of dealing with
this problem is to choose some upper limit, and use the strategy described in Known
Maximum Length Collection. If that limit is reached, then:
Typically, increasing the list length by a constant factor results in better performance
than increasing it by a constant number of entries:
rndUntil42 :=
proc()
local L, i;
begin
i := 1;
L := [random()];
while L[i] mod 42 <> 0 do
4-92
Data Collection
if i = nops(L) then
L := L.L;
end_if;
i := i+1;
L[i] := random();
end_while;
L[1..i];
end:
SEED := 123456789:
rndUntil42()
SEED := 123456789:
time(rndUntil42() $ i = 1..500)
Alternatively, if you cannot predict the number of elements that you will need to collect,
then use a table that grows automatically (a hash table):
rndUntil42 :=
proc()
local L, i, j;
begin
i := 1;
L := table(1 = random());
while L[i] mod 42 <> 0 do
i := i+1;
L[i] := random();
end_while;
[L[j] $ j=1..i];
end:
4-93
4 Programming Fundamentals
SEED := 123456789:
time(rndUntil42() $ i = 1..500)
For this example, using the table is slightly faster. If you change the value 42 to another
value, using the list might be faster. In general, tables are preferable when you collect
large amounts of data. Choose the approach that works best for solving your problem.
4-94
Visualize Expression Trees
4-95
4 Programming Fundamentals
prog::exprtree(a + b * c + d * e *sin(f)^g):
_plus
|
+-- a
|
+-- _mult
| |
| +-- b
| |
| `-- c
|
`-- _mult
|
+-- d
|
+-- e
|
`-- _power
|
+-- sin
| |
4-96
Visualize Expression Trees
| `-- f
|
`-- g
4-97
4 Programming Fundamentals
Modify Subexpressions
In this section...
“Find and Replace Subexpressions” on page 4-98
“Recursive Substitution” on page 4-101
evalAt replaces specified objects in the expression tree with the specified values or
subexpressions, and then evaluates the expression. This function replaces only entire
branches of expression trees. It cannot replace a part of the branch, for example, two
terms in the sum of three terms:
a*(b + c) | b = d,
a*(b + c) | b + c = d,
a*(b + c + 1) | b + c = d
4-98
Modify Subexpressions
subs replaces specified objects in the expression tree with the specified values or
subexpressions. This function cannot replace a part of the branch in the expression tree:
subs(a*(b + c), b + c = d),
subs(a*(b + c + 1), b + c = d)
After substitution, subs does not evaluate the resulting expression (although it can
simplify the expression). You can enforce evaluation of the modified subexpressions by
using the EvalChanges option:
subs(ln(x), x = E),
subs(ln(x), x = E, EvalChanges)
subs replaces both free and dependent identifiers. In some cases, replacing dependent
identifiers can cause invalid results:
subs(int(f(x), x = 0..1) + f(x), x = 100)
subsex analyzes associative system operators and can replace part of the branch in the
expression tree:
subsex(a*(b + c), b + c = d),
subsex(a*(b + c + 1), b + c = d)
subsex(ln(x + a + 1), x + a = E - 1)
4-99
4 Programming Fundamentals
eval(%)
subsop replaces only entire branches in the expression tree of an expression, the
same way as subs. When using subsop, you must know the position (index) of the
branch inside the expression in internal order that might differ from the output order
used to represent the expression on screen. To find the internal index of a particular
subexpression, use the op function:
op(ex, 2);
Now you can use subsop to replace the parameter a with some value. For example,
replace it with the value 3:
subsop(ex, [2, 1, 2, 1] = 3)
4-100
Modify Subexpressions
prog::find helps you find all occurrences of a specific value in the expression. For
example, find all sums in this expression:
ex := (x + 1)/(x^2 + 2*x - 2) - 1/x + 1/(x + 1):
pos := [prog::find(ex, hold(_plus))];
map(pos, p -> op(ex, p));
map(pos, p -> op(ex, p[1..-2]))
Recursive Substitution
You also can find all subexpressions of a particular type (for example, all Bessel functions
or all branches not containing x), execute some code for these subexressions and replace
them with the return value of that code. For this task, use the misc::maprec function.
Suppose you want to rewrite all terms that contain the sine and tangent functions in
terms of cosine functions. (In this example, do not use sin(x)2 = 1 - cos(x)2 and similar
identities.) First, create the functions sin2cos and tan2cos that rewrite expressions in
terms of the cosine function. These functions access the operands of the sine and tangent
functions using op(ex):
Now you can use these functions when replacing all occurrences of sine and
tangent functions in an expression. To replace subexpressions of the original
expression, use misc::maprec. The misc::maprec function uses the syntax
misc::maprec(expression, selector = replacement), where:
4-101
4 Programming Fundamentals
• selector is the selection criterion (for example, a set of types of subexpressions that
you want to replace).
• replacement is the procedure that you want to use to replace subexpressions of the
original expression.
Besides data types or types of expressions, such as "sin" or "tan", you can use
procedures to represent selection criteria in misc::maprec. In this example, the
selection criterion of misc::maprec is the procedure ex -> bool(freeIndets(ex)
= {}) that excludes free identifiers and selects all constants of an expression. Using the
procedure f as a replacement, misc::maprec replaces all nonrational constants of an
expression with new identifiers:
f :=
proc(x)
option remember;
begin
if testtype(x, Type::Rational) then x
else genident();
end_if;
end:
misc::maprec(a = 5*b + PI*sqrt(2)*c + PI,
(ex -> bool(freeIndets(ex) = {})) = f)
option remember in f ensures that constants appearing multiple times always get the
same identifier. Moreover, you can access the remember table of the procedure f and
select which substitutions you want to make:
4-102
Modify Subexpressions
select([op(op(f,5))], _not@bool)
4-103
4 Programming Fundamentals
Closures
When you call a procedure, MuPAD allocates memory for the local variables, marks them
as uninitialized, and evaluates the body of the procedure. At the end of a procedure call,
MuPAD destroys local variables freeing the allocated memory. Now suppose that the
result of a procedure call refers to local variables of that procedure. For example, the
returned value of this procedure refers to its local variable z:
f :=
proc(x, y)
local z;
begin
z := x + y;
return(z);
end:
In this case, the variable z is replaced by its value at the end of the procedure call.
Therefore, the returned value of the procedure is the value of the variable z, not the
variable z itself:
f(1, 2)
Use hold to suppress evaluation of the variable z. Now the procedure returns an object
of type DOM_VAR:
f :=
proc(x, y)
local z;
begin
z := x + y;
return(hold(z));
end:
4-104
Variables Inside Procedures
f(1, 2)
Objects of type DOM_VAR represent local variables and can only be used inside
procedures. An object of type DOM_VAR returned as a result of a procedure call is useless
because it does not have any connection to the procedure.
You can access local variables of a procedure if you either declare them in that procedure
or declare them in a lexically enclosing procedure. For example, in the following code the
procedure g can access and modify the variable x of the enclosing procedure f:
f :=
proc(x)
local g;
begin
g := proc()
begin
x := x+1;
end:
g();
end:
f(2)
Instead of returning the result of the procedure call g(), you can return g itself. In this
case, the returned value retains a link to the variable x of the procedure call. For reasons
of memory management, f must declare that it will return something holding a reference
to a local variable. To declare it, use option escape:
f :=
proc(x)
local g;
option escape;
begin
g := proc()
begin
x := x+1;
end:
g;
end:
4-105
4 Programming Fundamentals
h := f(2):
i := f(17):
h(); h(); i(); h()
Static Variables
Alternative to Static Variables in MuPAD
Many programming languages support the concept of static variables. Static variables
are local variables the values of which are not reset in each call to a procedure. The value
of a static variable is initialized during the first call to a procedure. In each subsequent
call, a procedure remembers the value of a static variable from the previous call.
Although MuPAD does not let you declare a variable inside a procedure as a static
variable, you can still use the concept of static variables while programming in MuPAD.
When defining a procedure with proc, you often assign the procedure to an identifier.
However, MuPAD lets you use anonymous procedures. Also, you can define one
procedure inside another procedure. Thus, you can implement the alternative to a static
variable in MuPAD as a nested procedure where:
1 The outer procedure has a local variable. The outer procedure can be anonymous.
2 The inner procedure uses the local variable of the outer procedure. For the inner
procedure that variable is not local, and therefore it does not reset its value in each
call.
4-106
Variables Inside Procedures
proc()
local cnt;
option escape;
begin
cnt := 0;
f :=
proc()
begin
cnt := cnt + 1;
end:
end():
f(); f(); f()
The technique of creating static variables in MuPAD lets you create shared static
variables by creating several inner procedures. The inner procedures use the same local
variable of the outer procedure. For inner procedures that variable is not local, and
therefore it does not reset its value:
proc()
local x, y;
option escape;
begin
x := 0;
y := 0;
f := () -> (x := x + y; [x, y]);
g := n -> (y := y + n; [x, y]);
end_proc():
f();
g(2);
f();
f()
4-107
4 Programming Fundamentals
4-108
Utility Functions
Utility Functions
In this section...
“Utility Functions Inside Procedures” on page 4-109
“Utility Functions Outside Procedures” on page 4-109
“Utility Functions in Closures” on page 4-111
The helper function is not visible or accessible outside f. Your users cannot see the
helper function, and therefore, they do not rely on a particular implementation of this
procedure. You can change its implementation without breaking their code. At the same
time, helper can access and modify arguments of f.
The major disadvantage of this approach is that your test files cannot access helper
directly. Since it is typically recommended to start testing at the smallest possible
building blocks, this is a real disadvantage. Nevertheless, for many tasks the benefits of
this approach prevail over this disadvantage, especially if the utility function must be
able to modify the arguments of the calling function.
4-109
4 Programming Fundamentals
f := funcenv(
proc(arguments)
local ...;
begin
... code using f::helper(...) ...
end):
f::helper :=
proc(...)
begin
...
end:
This approach does not require you to define the utility function in the function
environment of the procedure that uses it. Defining a utility function in the function
environment only helps you clarify to people reading your code that you intend to call
f::helper primarily or solely within f. If you later decide to use f::helper in another
procedure, you can move the utility function to a more generic utility library. Again, this
recommendation only helps you improve readability of your code.
Defining utility functions outside the procedure that uses them does not hide utility
functions. Therefore, this approach lets you:
Defining utility functions outside the procedure that uses them has the following
disadvantages:
• Your users can access utility functions. If they rely on a particular implementation of
the utility function, changing that implementation might affect their code.
• The utility function cannot access local variables of the procedure that uses that
utility function. The workaround is to pass these local variables as arguments to the
utility function.
• The utility function does not have privileged access to the arguments of the procedure
that uses that utility function.
• Defining the utility function far from the code line where you call it reduces
readability of the code.
4-110
Utility Functions
proc()
local helper;
option escape;
begin
helper :=
proc(...)
...
end:
f :=
proc(arguments)
local ...;
begin
... code using helper(...) ...
end:
g :=
proc(arguments)
local ...;
begin
... code using helper(...) ...
end:
end():
For details about such structures, see Closures and Static Variables.
If you define a utility function in a closure, that function is inaccessible to any external
code. Your users cannot see and, therefore, rely on a particular implementation of that
utility function. Changing it will not break their code. At the same time, this approach
lets you create more than one procedure that can access the utility function. In the
example, both f and g can access helper.
4-111
4 Programming Fundamentals
The disadvantage of this approach is that the helper function cannot access the local
variables of the procedures that use it. To overcome this limitation, you can use the
context function or shared static variables.
Note: Using context or shared static variables to make local variables of the calling
procedure accessible for the utility function is not recommended.
Using context to overcome this limitation typically leads to unreadable and difficult
to maintain code. The problems with shared static variables resemble the problems
with global variables, especially for recursive calls. The helper procedure can access and
modify such variables, but all other procedures inside the same outer procedure can
access and modify them too.
4-112
Private Methods
Private Methods
Although MuPAD does not let you declare a method as private, you can create private
methods by using closures.
MuPAD uses a fundamentally simple object and name lookup model. Objects are data
that belong to a particular domain type, and domains have named entries (called slots). If
the value of a slot is a function, this entry is called a method. Therefore, MuPAD lets you
use the same techniques for hiding method calls as you use for hiding utility functions.
For details, see Utility Functions in Closures.
This example creates the private method f of the domain d. This method is not accessible
from methods in inherited domains and from category methods:
domain d
local f;
inherits Dom::BaseDomain;
g := proc()
begin
print("g");
f();
end;
begin
f := proc()
begin
print("f");
end;
end:
d::f
d::g()
4-113
4 Programming Fundamentals
Calls by Value
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
4-114
Calls by Reference and Calls by Value
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.
Calls by Reference
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)
4-115
4 Programming Fundamentals
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.
• Lexical Scoping
• Closures in Objects
• Domains in Objects
• Context Switching
Lexical Scoping
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();
4-116
Calls by Reference and Calls by Value
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.
Closures in Objects
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;
begin
get := x -> extop(x, 1)();
set := (x, y) -> extop(x, 2)(y);
end_domain:
e := d(4)
d::incr(e)
4-117
4 Programming Fundamentals
Domains in Objects
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;
end_domain:
e := dd(4)
dd::incr(e)
4-118
Calls by Reference and Calls by Value
The primitives of the plot library use this strategy. For example, when you execute the
following code, the domain plot::Function2d overloads the slot function:
f := plot::Function2d(sin(x), x=-PI..PI):
f::Color := RGB::Green:
Context Switching
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:
++1
4-119
4 Programming Fundamentals
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.
4-120
Integrate Custom Functions into MuPAD
float(sin(1));
diff(sin(x), x, x, x);
expand(sin(x + 1))
You can say that the mathematical knowledge about the built-in functions is distributed
over several system functions: float knows how to compute numerical approximations
of the sine function, diff knows the derivative of the sine function, and expand knows
the addition theorems of the trigonometric functions.
When you implement your own function, you can integrate it into the MuPAD system,
making other functions aware how to work with this new function correctly. If the new
function consists only of built-in MuPAD functions, then you do not need to take extra
steps. All MuPAD functions interact correctly with the new function:
f := x -> (x*sin(x)):
diff(f(x), x)
However, if you implement a function that is not composed of the standard MuPAD
objects (for example, a new special function), you must distribute the knowledge about
the mathematical meaning of the new function to standard MuPAD functions, such as
diff, expand, float, and so on. This extra task is necessary for integrating the new
function with the rest of the system. For example, you might want to differentiate an
expression that contains both the new function and some built-in functions, and such
4-121
4 Programming Fundamentals
differentiation is only possible via the MuPAD differentiation routine. Therefore, this
routine must know how to handle the new symbol.
You can call a function environment as you would call any MuPAD function or procedure:
sin(1.7), exp(1.0)
Suppose you implement the complete elliptic integral functions of the first and second
kind, K(z) and E(z). These functions appear in different contexts, such as calculating
the perimeter of an ellipsis, the gravitational or electrostatic potential of a uniform ring,
and the probability that a random walk in three dimensions ever goes through the origin.
The elliptic integrals have the following special values:
, E(1) = 1, , .
MuPAD provides the built-in functions ellipticE and ellipticK for computing these
elliptic integrals. However, you can implement your own functions for the same task. For
example, write the procedures ellipE and ellipK. These procedures define the values
of the elliptic integrals for special values of x. For all other argument values, the values
of elliptic integrals are unknown, and the procedures return the symbolic expressions
ellipE(x) and ellipK(x). Use procname to return symbolic expressions:
ellipE :=
proc(x) begin
if x = 0 then PI/2
4-122
Integrate Custom Functions into MuPAD
elif x = 1 then 1
else procname(x) end_if
end_proc:
ellipK :=
proc(x) begin
if x = 0 then PI/2
elif x = 1/2 then 8*PI^(3/2)/gamma(-1/4)^2
elif x = -1 then gamma(1/4)^2/4/sqrt(2*PI)
else procname(x) end_if
end_proc:
ellipE and ellipK return special values for particular arguments. For all other
arguments, they return symbolic expressions:
ellipE(0), ellipE(1/2),
ellipK(12/17), ellipK(x^2 + 1)
, .
The standard MuPAD differentiation function diff does not know about these rules.
Therefore, trying to differentiate ellipE and ellipK simply returns the symbolic
notations of the derivatives:
diff(ellipE(x), x),
diff(ellipK(x), x)
To make diff work with the new functions, create function environments from the
procedures ellipE and ellipK. In addition, function environments let you control the
appearance of the symbolic function calls in outputs.
• The first operand is a procedure that computes the return value of a function call.
4-123
4 Programming Fundamentals
• The second operand is a procedure for printing a symbolic function call on the screen.
• The third operand is a table that specifies how the system functions handle symbolic
function calls.
Although ellipE and ellipK are now function environments, you can call them as you
would call any other MuPAD function:
ellipE(0), ellipE(1/2),
ellipK(12/17), ellipK(x^2+1)
The third argument funcenv is a table of function attributes. It tells the system
functions (such as float, diff, expand, and so on) how to handle symbolic calls of the
form ellipE(x) and ellipK(x). You can update this table specifying the rules for
the new function. For example, specify the new differentiation rules by assigning the
appropriate procedures to the diff slot of the function environments:
ellipE::diff :=
proc(f,x)
local z;
begin
z := op(f);
(ellipE(z) - ellipK(z))/(2*z) * diff(z, x)
end_proc:
ellipK::diff :=
proc(f,x)
local z;
begin
z := op(f);
(ellipE(z) - (1-z)*ellipK(z))/
4-124
Integrate Custom Functions into MuPAD
(2*(1-z)*z) * diff(z, x)
end_proc:
Now, whenever f = ellipE(z), and z depends on x, the call diff(f, x) uses the
procedure assigned to ellipE::diff:
diff(ellipE(z), z);
diff(ellipE(y(x)), x);
diff(ellipE(x*sin(x)), x)
Since the taylor function internally calls diff, the new differentiation routine also lets
you compute Taylor expansions of the elliptic integrals:
taylor(ellipK(x), x = 0, 6)
If a derivative of a function contains the function itself, the integration routine has a
good chance of finding symbolic integrals after you implement the diff attributes. For
example, int now computes the following integrals:
4-125
4 Programming Fundamentals
int(ellipE(x), x)
int(ellipK(x), x)
4-126