Contents Previous Next Index
8 Programming with Modules
This chapter describes the structure and flexibility of Maple modules.
Modules allow you to associate related procedures and data in one structure. By creating a module, you can write code that can be reused, transported, and easily maintained. You can also use modules to implement objects in Maple.
This chapter provides several example modules, many of which are available as Maple source code in the samples directory of your Maple installation. You can load these examples into the Maple library to modify and extend them, and use them in custom programs.
8.1 In This Chapter
Syntax and semantics
Using modules as records or structures
Modules and use statements
Interfaces and implementations
8.2 Introduction
You may decide to create a module for one of the purposes described below.
Encapsulation
Encapsulation is the act of grouping code together in one structure to separate its interface from its implementation. By doing so, you can create applications that are transportable and reusable and that offer well-defined user interfaces. This makes your code easier to maintain and understand--important properties for large software systems.
Creating a Custom Maple Package
A package is a means of bundling a collection of Maple procedures related to a domain. Most of the Maple library functionality is available in packages.
Creating Objects
Objects can be represented using modules. In software engineering or object-oriented programming, an object is defined as an element that has both a state and behavior. Objects are passed the same way as ordinary expressions, but also provide methods which define their properties.
Creating Generic Programs
Generic programs accept objects with specific properties or behaviors. The underlying representation of the object is transparent to generic programs. For example, a generic geometry program can accept any object that exports an area method, in addition to other objects. The framework of the program would rely on information in each given object to determine specific behaviors, while the overall program implements a common pattern between the objects.
8.3 A Simple Example
In the following example, a module generates a sequence of numbers.
Counter := module() description "number generator"; export getNext; local count; count := 0; getNext := proc() count := 1 + count; end proc; end module: Counter:-getNext(); Counter:-getNext(); Counter:-getNext();
1
2
3
The module definition format, which will be described in more detail in the next section, is similar to a procedure definition in that the body is contained within a delimited code block. Also, elements such as local variables, options, and description are declared at the top of the module. Unlike a procedure, the body of the module is evaluated only once when it is declared. The values that are defined during this evaluation process, and the values that are defined in subsequent usage of the module, are stored and can be used again.
In a module definition, you can declare exported variables, which are names that will be made available once the module has been run. These exported variables can be accessed by using the member selection operator (:-) or the indexing operation ( [] ) , while local variables remain private (that is, they are accessible only by methods within the module). The example above declares and uses one exported local variable called getNext and one local variable called count.
8.4 Syntax and Semantics
Module definitions have the following general syntax.
module() local L; export E; global G; options O; description D; B end module
The Module Definition
All module definitions begin with the keyword module, followed by an empty pair of parentheses. This is similar to the parentheses that follow the proc keyword in a procedure definition. Following that is an optional declaration section and the module body. The keywords end module (or simply end) terminate a module definition.
The simplest valid module definition is
module() end;
module...end module
which does not contain exported variables, local variables, references, global variables, or a body of statements.
The Module Body
The body of a module definition contains the following components:
Zero or more Maple statements. The body is executed when the module definition is evaluated, producing a module.
Assignment statements that assign values to the exported names of the module.
Also, the body can optionally contain the following components:
Assignments to local variables and arbitrary computations.
A return statement, which cannot contain a break or next statement outside of a loop. Running a return statement terminates the execution of the body of the module definition.
Module Parameters
Unlike procedures, module definitions do not have explicit parameters because modules are not called (or invoked) with arguments.
Implicit Parameters
All module definitions have an implicit parameter called thismodule. Within the body of a module definition, this special name evaluates to the module in which it occurs. You can, therefore, refer to a module within its own definition before the result of evaluating it has been assigned to a name.
thismodule is similar to thisproc in procedures, but is not the same as procname. The difference between thismodule and procname is that procname evaluates to a name, while thismodule evaluates to the module expression itself. There is no concept of a modulename implicit variable because the invocation phase of evaluating a module definition is part of its normal evaluation process, and it occurs immediately. Procedures, on the other hand, are not invoked until they are called with arguments. Normally, at least one name for a procedure is known by the time it is called; this is not the case for modules.
Implicit parameters related to passing arguments (for example, _params, _options, _passed, and others) cannot be referenced in module definitions. They are only available within the scope of a procedure.
For more information on procedures, see Procedures.
Named Modules
In a module definition, an optional symbol can be specified after the module keyword. Modules created in this way are called named modules.
Semantically, named modules are almost identical to normal modules, but the exported variables of named modules are printed differently, allowing the module from which it was exported to be identified visually. In the following example, a normal module is assigned to the name NormalModule.
NormalModule := module() export e; end module; NormalModule:-e;
NormalModule≔module...end module
e
In the following example, the symbol (the name of the module) after the module keyword is NamedModule.
module NamedModule() export e; end module;
moduleNamedModuleexporte;end module
NamedModule:-e;
NamedModule:−e
When the definition of a named module is evaluated, the name (which appears immediately after the module keyword) is assigned the module as its value and the name is protected (that is, it cannot be modified). Therefore, a named module is usually created only once. For example, an error occurs when the same named module definition above is executed.
Error, (in NamedModule) attempting to assign to `NamedModule` which is protected. Try declaring `local NamedModule`; see ?protect for details.
Executing the normal module definition again creates a new instance of the module and does not result in an error. It simply reassigns the variable NormalModule to the new module instance.
NormalModule := module() export e; end module;
If you save a normal module to a Maple library archive, which is a file used to store a collection of internal files, the normal module becomes a named module the next time it is loaded from the library archive. The savelib command, which is the command used to save a file to a library archive, takes the name of the variable assigned a module, and saving the file associates this name with the module.
For more information about library archive files, see Writing Packages.
Important: Do not assign a named module to another variable, for example,
SomeName := eval( NamedModule );
SomeName≔moduleNamedModuleexporte;end module
SomeName:-e;
Exports of named modules are printed using the distinguished name that was given to the module when it was created, regardless of whether it has been assigned to another name.
Whether a module has a name also affects the reporting of errors that occur during its evaluation. When the second attempt to evaluate the named module definition above generated an error, the error message reported the location of the error by name. In contrast, when an error occurs during the evaluation of a normal module definition, the name anonymous is used instead.
NormalModule := module() export e; error "oops"; end module;
Error, (in anonymous module) oops
This process differs from procedure error reporting. Maple cannot report the name of a normal module (that is, the name of the variable to which the module is assigned) because the evaluation of the right-hand side of an assignment occurs before the assignment to the name takes place. Therefore, the error occurs before the association between a variable and the module has occurred.
Declarations
The declarations section of the module must appear immediately after the parentheses. All of the statements in the declarations section are optional, but, at most, one of each kind can be specified. Most module declarations are the same as those for procedures.
For more information, see Parameter Declarations.
Description Strings
You can provide a brief description to summarize the purpose and function of your modules. Providing a description is valuable to other users who read your code. Include text after the description keyword as you would in a procedure definition.
Hello := module() description "my first module"; export say; say := proc() print( "HELLO WORLD" ) end proc; end module:
When the module is printed, its description string is displayed.
eval( Hello );
The export declaration is described later in this chapter.
Global Variables
Global variables referenced in a module definition are declared by using the global keyword. Following the global keyword is a sequence of one or more symbols, which are associated with their global module instances. In certain cases, you must declare a name as a global variable to prevent implicit scoping rules from making it local.
Hello := module() export say; global message; say := proc() message := "HELLO WORLD!" end proc; end module:
message;
message
Hello:-say();
HELLO WORLD!
Local Variables
You can define variables that are local to the module definition by using the local declaration. Its format is the same as for procedures. The following example is a variant of the previous Hello module, which uses a local variable.
Hello := module() local loc; export say; loc := "HELLO WORLD!"; say := proc() print( loc ) end proc; end module:
Local variables (or locals) cannot be used or changed outside of the module definition in which they occur. In other words, they are private to the module.
A local variable in a module is a distinct object from a global variable with the same name. While local variables in procedures are typically used only for the duration of the execution time of the procedure body, module local variables are stored after the module definition is executed. They can be used to maintain a state. For example, in the Counter example described at the beginning of this chapter, a local count variable stores the current value of the counter. The count local variable increments each time the getNext procedure is invoked. Its new value is stored and can be used the next time the procedure is called. At the same time, because count is local, no external programs can change its value and end the sequence defined by the module.
Exported Local Variables
Procedures and modules both support local variables. However, only modules support exported local variables, which are often called exports.
Module exports are declared by using the export declaration. It begins with the keyword export, followed by a (nonempty) sequence of symbols. A name is never exported implicitly; exports must be declared.
The result of evaluating a module definition is a module. You can view a module as a collection of its exports, which are also referred to as members of the module. These are simply names that can (but need not) be assigned values. You can establish initial values for the exports by assigning values to them in the body of the module definition.
The word export is a short form for exported local variable. In most cases, a module export is a local variable such as those declared with the local declaration. The difference is that you can access the exported local variables of a module after it has been created.
To access an export of a module, use the member selection operator (:-). Its general syntax is
modexpr :- membername
modexpr must be an expression that evaluates to a module and membername must be the name of an export of the module to which modexpr evaluates. Anything else signals an exception. You cannot access the local variables of an instantiated module by using this syntax.
The Hello example above has one export named say. In the following example, say is assigned a procedure. To call it, enter
The following expression raises an exception because the name noSuchModule is not assigned a module expression.
noSuchModule:-e;
Error, `noSuchModule` does not evaluate to a module
In the following example, a module expression is assigned to the name m and the member selection expression m:-e evaluates to the value of the exported variable e of m.
m := module() export e; e := 2 end module: m:-e;
Since m does not export a variable named noSuchExport, the following expression raises an exception.
m:-noSuchExport;
Error, module does not export `noSuchExport`
In addition to the :- syntax, square brackets can also be used to reference a module export.
m[e];
The square bracket notation has different evaluation rules than member selection. When using the member selection operator (:-), the export name must be known in advance. When using [], the name of the export can be computed. In this example, an exported variables value can be selected from an arbitrary module.
m := module() export a := 1, b := 2, c := 3; end module: FirstExport := proc( m::`module` ) local ex := exports(m); return m[ex[1]]; end proc; FirstExport(m);
FirstExport≔procm::modulelocalex;ex ≔ exports⁡m;returnm[ex[1]]end proc
Important: Exports do not need to have assigned values. The following module exports an unassigned name. This illustrates the importance of distinguishing module exports from global variables.
m := module() export e; end module:
References to the exported name e in m evaluate to the name e.
m:-e;
Note, however, that this is a local name e and not the global instance of the name.
evalb( e = m:-e );
false
The first e in the previous expression refers to the global e, while the expression m:-e evaluates to the e that is local to the module m. This distinction between a global and export of the same name is useful. For example, you can create a module with an export sin. Assigning a value to the export sin does not affect the protected global name sin.
Determining the Export Names
You can determine the names of the exports of a module by using the exports procedure.
exports( Hello );
say
exports( NormalModule );
This procedure returns the global instances of the export names.
exports( m );
evalb( (21) = e );
true
You can also obtain the local instances of those names by using the option instance.
exports( m, 'instance' );
evalb( (23) = e );
evalb( (23) = m:-e );
You cannot have the same name declared as both a local variable and an exported local variable.
module() export e; local e; end module;
Error, (in anonymous module) declaration of local variable `e` masks an earlier exported declaration
(The declared exports and locals actually form a partition of the names that are local to a module.)
Testing for Membership in a Module
As described in previous chapters, the member command can be used to test whether a value is a member of a set or list.
member( 4, { 1, 2, 3 } );
This command can also be used for membership tests in modules.
member( say, Hello );
member( cry, Hello );
The first argument is a global name whose membership is to be tested, and the second argument is the name of a module. The member command returns the value true if the module has an export whose name is the same as the first argument.
The member command also has a three-argument form that can be used with lists to determine the first position at which an item occurs.
member( b, [ a, b, c ], 'pos' );
The name pos is now assigned the value 2 because b occurs at the second position of the list. [ a, b, c].
pos;
When used with modules, the third argument is assigned the local instance of the name whose membership is being tested, provided that the return value is true.
member( say, Hello, 'which' );
which;
eval( which );
procprint⁡locend proc
If the return value from the member command is false, the name remains unassigned or maintains its previously assigned value.
unassign( 'which' ):
member( cry, Hello, 'which' );
which
Module Options
Similar to procedures, a module definition can contain options. The options available for modules are different from those for procedures. Only the options trace and copyright are common to both procedures and modules. The following four options have a predefined meaning for modules: load, unload, package, and record. The load and unload options cover functionality defined by the ModuleLoad and ModuleUnload special exports described in the next section.
For more information, refer to the module,option help page.
The package Option
A package is a collection of procedures and other data that can be treated as a whole. Packages typically gather several procedures that allow you to perform computations in a well-defined problem domain. Packages can contain data other than procedures and can even contain other packages (subpackages).
The package option is used to designate a module as a Maple package. The exports of a module created with the package option are automatically protected.
For more information, see Writing Packages.
The record Option
The record option is used to identify records, which are fixed-size collections of items. Records are created by using the Record constructor and are represented using modules.
For more information, see Records.
Special Exports
Certain specially named exports, when defined in a module, affect how modules behave in Maple. These special exports are described below. In most cases, they can be declared as either exported local variables or local variables.
The ModuleApply Procedure
When a procedure named ModuleApply is declared as an export or local of a module, the module name can be used as if it were a procedure name.
Consider the Counter example described at the beginning of this chapter. Since it only has one method, the calling sequence can be shortened by using the ModuleApply function.
Counter := module() export ModuleApply; local count; count := 0; ModuleApply := proc() count := 1 + count; end proc; end module: Counter(); Counter(); Counter();
In this example, calls to Counter:-ModuleApply() are not needed and the results are the same as those generated by the original Counter example. The ModuleApply function can specify and accept any number of parameters.
You can also use the ModuleApply function to create module factories, a standard object-oriented design pattern described later in this chapter.
The ModuleIterator Procedure
The ModuleIterator procedure defines how a module functions when it is used as the in clause of a for loop.
for e in myModule do # Do something with e end do; for i, e in myModule do # Do something with e, whose index in myModule is i. end do;
In the example below, the ModuleIterator procedure returns two procedures: hasNext and getNext. These procedures can have any names, and in fact, do not require names. When the ModuleIterator procedure is called, an iterator is initialized for the instance, the details of which are kept hidden from the caller. The two returned procedures can then be used to iterate over the instance to perform a specific task. For example, consider a class that implements a form of a set of which mySet is an instance. You can iterate over this set as follows (passing i is only necessary if the index is needed):
(hasNext,getNext) := ModuleIterator(mySet); while hasNext() do e := getNext('i'); # Do something with e, whose index is in i. end do;
The example above is an explicit use of the ModuleIterator procedure. However, this mechanism is also used implicitly by the Maple for-loop construct,
The hasNext procedure returns a value of true or false depending on whether remaining elements need to be processed. Successive calls to hasNext with no intervening calls to getNext return the same result. The getNext procedure returns the next element to process, and increments the iterator. If an argument was passed to getNext, it will be an unevaluated name, and getNext should assign to it an index corresponding to the iterator (one that has meaning to some other procedure in the module that can be used to look up an entry using this index). If the concept of an index does not make sense for this particular module, getNext should not perform the assignment. These procedures should be implemented so that it is always safe to call getNext after the most recent call to hasNext returns a value of true. The result of calling getNext after hasNext has returned a value of false, or before hasNext has ever been called, is up to the implementer of the class.
The following example implements a fixed-size container of a collection of strings. In a real-world use case, these might have been other values that are expensive to compute, so we allocate this module to collect them for convenient access.
CommonWords := module() export ModuleIterator, lookup; local words := [ "the", "be", "to", "of", "and" ]; ModuleIterator := proc() local i := 1; return ( # This is the (anonymous) hasNext procedure. proc() evalb( i <= numelems(words) ) end proc, # This is the getNext procedure. proc(returnIndex := NULL) local word := words[i]; # If a name was passed to hasNext, assign it the index. if returnIndex <> NULL then returnIndex := i end if; i := i + 1; word end proc ) end proc; # This procedure can use the indices returned by getNext. lookup := proc(index) words[index] end proc; end module;
CommonWords≔module...end module
This loop will enumerate only the words in the CommonWords module:
for e in CommonWords do e end do;
the
be
to
of
and
This loop will enumerate both the words and their indices:
for i, e in CommonWords do printf( "'%s' has index %d\n", e, i ); end do;
'the' has index 1 'be' has index 2 'to' has index 3 'of' has index 4 'and' has index 5
Notice that the ModuleIterator procedure declares a local variable, i, that is used as the index by hasNext and getNext. If this variable were declared local to the entire module, then it would only be possible to iterate over it once, as i would never get reset to 1. By declaring i local to ModuleIterator, it becomes part of the environment for the instances of hasNext and getNext returned by a particular call to ModuleIterator. Thus, multiple iterators into the same module can exist simultaneously without interfering with one another. For example:
for i, e in CommonWords do printf( "'%s' has index %d\n", e, i ); if i = 3 then for j, f in CommonWords do printf( "\t'%s' has index %d\n", f, j ) end do end if end do;
'the' has index 1 'be' has index 2 'to' has index 3 'the' has index 1 'be' has index 2 'to' has index 3 'of' has index 4 'and' has index 5 'of' has index 4 'and' has index 5
When the module iterator is used by the seq, add, or mul commands, Maple first checks if the module is an object that exports the numelems command. If so, it will call the numelems command to determine how large to preallocate the result, and the hasNext and getNext procedures must return exactly that many elements. If the module does not export a numelems method, Maple will enlarge the result as needed, which will consume more space (as intermediate results are discarded) and time (garbage collection), although Maple will try to do this as efficiently as possible.
The ModuleLoad Procedure
The ModuleLoad procedure is executed automatically when a module is loaded from the Maple library archive in which it has been saved. In a normal session, initialization code can be included in the module body. When loading a saved module, extra initialization code is sometimes required to set up run-time properties for the module. For example, a module that loads procedures from a dynamic-link library (.dll) file may need to call the define_external function during the initialization process. For more information on the define_external function, see Advanced Connectivity.
Consider the Counter example at the beginning of the chapter. The count index can have any value when it is saved. The next time you use it, you might want to reset the count to zero so that it is ready to start a new sequence. This can be done by using the ModuleLoad procedure.
Counter := module() export getNext, ModuleLoad; local count; ModuleLoad := proc() count := 0; end proc; ModuleLoad(); getNext := proc() count := 1 + count; end proc; end module: Counter:-getNext();
Note that the initialization code is contained within the ModuleLoad procedure. After that, the ModuleLoad procedure is also called. By defining the module in this way, you will get the same results when executing the module definition as you would when loading a saved module from a library archive.
The results of ModuleLoad can be duplicated using a procedure with a different name by using the load=pname option in the option sequence of the module.
ModulePrint
If a module has an export or local named ModulePrint, the result of the ModulePrint command is displayed instead of the module when a command in that module is executed.
The ModulePrint procedure does not display output. Instead, it returns a standard Maple expression that will be displayed. The expression returned can be customized to another object that portrays or summarizes the module.
In the following example, the Counter example will be extended from the ModuleIterator example to display a summary of what the module does.
Counter := module() export ModuleIterator, getNext, lower := 0, upper := 5; local ModulePrint, hasNext, count := 0; hasNext := proc() evalb( count < upper ); end proc; getNext := proc() count := 1 + count; return count ; end proc; ModuleIterator := proc() return hasNext, getNext; end proc; ModulePrint := proc() return [[ sprintf("Counter from %d to %d", lower, upper) ]]; end proc; end module;
Counter≔Counter from 0 to 5
ModuleUnload
The ModuleUnload procedure is called immediately before a module is discarded. A module is discarded either when it is no longer accessible and is garbage collected, or when you end your Maple session.
M := module() export ModuleUnload; ModuleUnload := proc() print("I am gone"); end proc; end module: unassign(M); 1;2;3;4; gc();
4
I am gone
You may not see the "I am gone" message after executing the code above because several factors determine exactly when memory is free to be garbage collected. At a minimum, no references can be left in the module. It must not be assigned or contained in any other live expression. This includes the ditto operators and the list of display reference handles (that is, the undo/redo buffer of the GUI). Also, it must not be identified as being alive by the garbage collector (i.e. a reference to the module is not found by the collector).
A module can become inaccessible, and therefore subject to garbage collection before the unload= procedure is executed, but can then become accessible again when that procedure is executed. In that case, the module is not garbage collected. When it eventually is garbage collected, or if you end your Maple session, the unload= procedure is not executed again.
The behavior of ModuleUnload can be duplicated using a procedure with a different name by using the unload=pname option in the option sequence of the module.
Implicit Scoping Rules
The bindings of names that appear within a module definition are determined when the module definition is simplified. Module definitions are subject to the same implicit scoping rules that apply to procedure definitions. Under no circumstances is a name ever implicitly determined to be exported by a module; implicitly scoped names can resolve only to non-exported local variables or global names.
Lexical Scoping Rules
Module definitions, along with procedure definitions, follow standard lexical scoping rules.
Modules can be nested, in the sense that a module can have any of its exports assigned to a module whose definition occurs within the body of the outer module.
Here is a simple example of a submodule.
m := module() export s; s := module() export e; e := proc() print( "HELLO WORLD!" ) end proc; end module end module:
The global name m is assigned a module that exports the name s. Within the body of m, the export s is assigned a module that exports the name e. As such, s is a submodule of m. The Shapes package, which is described in Writing Packages, illustrates the use of submodules.
Modules and procedures can both be nested to an arbitrary depth. The rules for the accessibility of local variables (including exported locals of modules) and procedure parameters are the same as the rules for nested procedures.
Module Factory
The Counter example used up to this point would be more useful if you could have many Counter modules running at the same time, and if they could be specialized according to specified bounds. Modules do not take explicit parameters, but you can write a generic module that could be specialized by using the factory design pattern.
To do this, write a constructor procedure for the module that accepts the lower and upper bound values as arguments. The following module creates a Counter.
MakeCounter := proc( lower::integer, upper::integer ) return module() export ModuleIterator, getNext; local ModulePrint, hasNext, count := lower; hasNext := proc() evalb( count < upper ); end proc; getNext := proc() count := 1 + count; return count ; end proc; ModuleIterator := proc() return hasNext, getNext; end proc; ModulePrint := proc() return [[ sprintf("Counter from %d to %d", lower, upper) ]]; end proc; end module; end proc; c1 := MakeCounter(6,10); c1:-getNext(); c1:-getNext(); c2 := MakeCounter(2,4); c2:-getNext(); c1:-getNext();
MakeCounter≔proclower::integer,upper::integerreturnmodulelocalModulePrint,hasNext,count;exportModuleIterator,getNext;count ≔ lower;hasNext ≔ procevalb⁡count<upperend proc;getNext ≔ proccount ≔ 1+count;returncountend proc;ModuleIterator ≔ procreturnhasNext,getNextend proc;ModulePrint ≔ procreturnsprintf⁡Counter from %d to %d,lower,upperend procend moduleend proc
c1≔Counter from 6 to 10
7
8
c2≔Counter from 2 to 4
9
In the above example, two specialized Counters operate at the same time with different internal states.
Modules and Types
Two Maple types are associated with modules. First, the name module is a type name. Naturally, an expression is of type module only if it is a module. When used as a type name, the name module must be enclosed in name quotes (`).
type( module() end module, '`module`' );
type( LinearAlgebra, '`module`' );
Second, a type called moduledefinition identifies expressions that are module definitions. In the previous example, the module definition
module() end module:
was evaluated before being passed to type, so the expression that was tested was not the definition, but the module to which it evaluates. You must use unevaluation quotes (') to delay the evaluation of a module definition.
type( 'module() end module', 'moduledefinition' );
Other important type tests satisfied by modules are the types atomic and last_name_eval.
type( module() end module, 'atomic' );
The procedure map has no effect on modules; modules passed as an argument to map remain unchanged.
map( print, module() export a, b, c; end module );
Modules also follow last name evaluation rules. For more information on last name evaluation rules, refer to the last_name_eval help page.
m := module() end module: m; type( m, 'last_name_eval' );
m
Although the type module is a surface type, which checks information at the top level of your code, it acts also as a structured type. Parameters passed as arguments to the unevaluated name module are interpreted as export names. For example, the module
m := module() export a, b; end module:
has the structured module type `module`( a, b ):
type( m, '`module`( a, b )' );
It also has the type `module`( a )
type( m, '`module`( a )' );
because any module that exports symbols a and b is a module that exports the symbol a.
For more information about structured types, refer to the type,structure help page.
8.5 Records
The Record command, which was introduced in Records, is an example of a module factory that can help you to write reusable code. Like an Array, a record is a fixed-size collection of items but, like a table, individual items stored within the record can be referenced by a name, rather than a numeric offset. In Maple, records, which are called structures in C++, are implemented as modules.
Creating Records
To create a record, use the Record constructor. In the simplest form, it takes the field names as arguments.
rec := Record( 'a', 'b', 'c' );
rec≔Record⁡a,b,c
The name rec is now assigned a record with fields named a, b, and c. You can access and assign values to these fields by using the expressions rec:-a, rec:-b, and rec:-c.
rec:-a := 2;
a≔2
rec:-a;
If unassigned, a record field evaluates to the local instance of the field name.
rec:-b;
b
evalb( (54) = b );
This is useful because the entire record can be passed as an aggregate data structure.
The record constructor accepts initializers for record fields. That is, you can specify an initial value for any field in a new or unassigned record by passing an equation with the field name on the left side and the initial value on the right.
r := Record( 'a' = 2, 'b' = sqrt( 3 ) );
r≔Record⁡a=2,b=3
r:-b;
In addition, you can associate Maple types with record fields. To associate a type, use the `::` operator with the field name specified as the first operand.
Type assertions can be used in combination with initializers. An incompatible initializer value triggers an assertion failure when the assertlevel kernel option is set to 2. For more information, refer to the kernelopts help page.
kernelopts( 'assertlevel' = 2 ):
Record( a::integer = 2.3, b = 2 );
Record⁡a::ℤ=2.3,b=2
r := Record( 'a'::integer = 2, 'b'::numeric );
r≔Record⁡a::ℤ=2,b::numeric
r:-b := "a string";
Error, assertion failed in assignment, expected numeric, got a string
If the initializer for a record field is a procedure, you can use the reserved name self to refer to the record you are creating. This allows records to be self-referential. The name self is applicable only to creating records and not to modules in general. For example, you can write a complex number constructor as follows.
MyComplex := ( r, i ) -> Record( 're' = r, 'im' = i, 'abs' = (() -> sqrt( self:-re^2 + self:-im^2 )) ):
c := MyComplex( 2, 3 ):
c:-re, c:-im, c:-abs();
2,3,13
Combined with prototype-based inheritance, described in Object Inheritance, this facility makes the Record constructor a powerful tool for object-oriented programming.
Record Types
Expressions created with the Record constructor are of the type record.
type( rec, 'record' );
This is a structured type that works the same way as the `module` type, but recognizes records specifically.
r := Record( a = 2, b = "foo" ):
type( r, 'record( a::integer, b::string )' );
Note: In a record type, the field types are used to test against the values assigned to the fields (if any), and are not related to type assertions on the field names (if any).
r := Record( a::integer = 2, b::{symbol,string} = "foo" ):
type( r, 'record( a::numeric, b::string )' );
Using Records to Represent Quaternions
Records can be used to implement simple aggregate data structures for which you want named access to slots. For example, four real numbers can be combined to form a quaternion and you can represent this using a record structure as follows.
MakeQuaternion := proc( a, b, c, d ) Record( 're' = a, 'i' = b, 'j' = c, 'k' = d ) end proc:
z := MakeQuaternion( 2, 3, 2, sqrt( 5 ) );
z≔Record⁡re=2,i=3,j=2,k=5
In this example, z represents the quaternion 2 + 3i + 2j + sqrt(5)*k (where i, j, and k are the nonreal quaternion basis units). The quaternion records can now be manipulated as single quantities. The following procedure accepts a quaternion record as its only argument and computes the Euclidean length of the quaternion that the record represents.
qnorm := proc( q ) use re = q:-re, i = q:-i, j = q:-j, k = q:-k in sqrt( re * re + i * i + j * j + k * k ) end use end proc:
qnorm( z );
22
A Maple type for quaternions can be introduced as a structured record type.
TypeTools:-AddType( 'quaternion', 'record( re, i, j, k )' );
type( z, 'quaternion' );
Object Inheritance
The Record constructor supports a simple form of prototype-based inheritance. An object system based on prototypes does not involve classes; instead, it uses a simpler and more direct form of object-based inheritance. New objects are created from existing objects (called prototypes) by cloning, that is, by copying and augmenting the data and behavior of the prototype.
The Record constructor supports prototype-based inheritance by accepting an index argument, which is the prototype for the new object record.
p := Record( a = 2, b = 3 ); # create a prototype
p≔Record⁡a=2,b=3
p:-a, p:-b;
2,3
r := Record[p]( c = 4 );
r≔Record⁡a=2,b=3,c=4
r:-a, r:-b, r:-c;
2,3,4
In this example, the record p is the prototype, and the second record r inherits the fields a and b, and their values, from the prototype p. It also augments the fields obtained from p with a new field c. The prototype p is not changed.
r:-a := 9;
a≔9
p:-a;
Behavior, as well as data, can be copied from a prototype. To copy behavior, use a constructor procedure for both the prototype and its clones.
BaseComplex := proc( r, i ) Record( 're' = r, 'im' = i ) end proc: NewComplex := proc( r, i ) Record[BaseComplex(r,i)]( 'abs' = (() -> sqrt( self:-re^2 + self:-im^2 )) ) end proc:
c := NewComplex( 2, 3 ):
An object created from a prototype can serve as a prototype for another object.
NewerComplex := proc( r, i ) Record[NewComplex(r,i)]( 'arg' = (() -> arctan(self:-im,self:-re)) ) end proc:
c2 := NewerComplex( 2, 3 ):
c2:-re, c2:-im, c2:-abs(), c2:-arg();
2,3,13,arctan⁡32
Note: Prototypes are supertypes of their clones.
subtype( 'record( re, im, abs )', 'record( re, im )' );
For example, NewComplex creates objects of a type that is a subtype of the objects created by BaseComplex.
8.6 Modules and use Statements
The use statement is designed to complement modules and to make programming with modules easier in some cases.
This section describes how the use statement can be used with modules. For more information about the use statement, see The use Statement.
A module m can appear in the binding sequence of a use statement. The module is regarded as an abbreviation for the sequence of equations a = m:-a, b = m:-b, ..., where a, b, ... are the exports of the module m.
For example,
m := module() export a, b; a := 2; b := 3; end module: use m in a + b end use;
5
This is useful for programming with packages.
m := Matrix( 4, 4, [[ 26, 0, 0, 30 ], [ 0, -41, -90, 0], [ 0, -7, -56, 0 ], [ 0, 0, 0, 0]] ); use LinearAlgebra in Determinant( m ); Rank( m ); CharacteristicPolynomial( m, 'lambda' ) end use;
m≔
0
λ4+71⁢λ3−856⁢λ2−43316⁢λ
Note that a name that appears in a binding list for a use statement, which is intended to be a module, must evaluate to a module at the time the use statement is simplified. This is necessary because the simplification of the use statement must be able to determine the exports of the module. For example, the following attempt to pass a module as a parameter to a procedure does not work, and an error occurs when the procedure is simplified.
proc( m, a, b ) use m in e( a, b ) end use end proc;
Error, (in anonymous procedure) no bindings were specified or implied
The correct way to use a module as a parameter is to specify the names to be bound explicitly, for example,
proc( m, a, b ) use e = m:-e in e( a, b ) end use end proc;
procm,a,bm:-e⁡a,bend proc
This is necessary because, until the procedure is called with a module expression as first argument, the reference to e is ambiguous. The variable e could refer to a module export or to another value (such as a global name). To expand the use statement, this must be known at the time the procedure is simplified.
Operator Rebinding
The use statement also allows most infix and prefix operators in the Maple language to be rebound. This is not operator overloading, which can be performed in some programming languages (such as C++), because the rebinding occurs during the automatic simplification process in Maple.
If an operator name appears on the left side of a binding equation for a use statement (consequently, if it is an exported name of a module that is bound with use), then the corresponding operator expressions in the body of the use statement are transformed into function calls. For example,
use `+` = F in a + b end use; m := module() export `*`, `+`; `+` := ( a, b ) -> a + b - 1; `*` := ( a, b ) -> a / b; end module: s * ( s + t ); use m in s * ( s + t ) end use;
F⁡a,b
s⁢s+t
ss+t−1
When a module-based package is loaded by running the with command, all of exported operators are rebound at the top level so you do not need to write use statements to get the overloaded implementations. If a module, M, exports a procedure named +, and you use the command with(M), subsequent sums will be processed through M:-+.
In most cases, the new operator procedure should contain the overload function. This provides a softer binding where your operator implementation will only be invoked when the arguments passed in match the specified type.
PairMath := module() option package; export `+`; `+` := proc( a::PAIR(integer,integer), b ) option overload; if type(b,PAIR(integer,integer)) then PAIR( op(1,a) + op(1,b), op(2,a) + op(2,b) ); else PAIR( op(1,a) + b, op(2,a) + b ); end if; end proc; end module; with(PairMath); PAIR(2,3) + 4; PAIR(1,1) + PAIR(3,4); 1+1;
PairMath≔module...end module
`+`
PAIR⁡6,7
PAIR⁡4,5
In the example above, PairMath:-+ will only be invoked when the left side of + is a PAIR structure. No error occurs when computing 1+1, which is not handled by PairMath:-+ because option overload has been specified for the PairMath:-+ procedure. When option overload is specified, a mismatched type simply moves on to the next + implementation.
Bypassing the current overload occurs on a mismatched parameter type check, or on any invalid input: exception raised within the procedure. The module above can be rewritten as follows.
PairMath := module() option package; export `+`; `+` := proc( a, b ) option overload; if type(a,PAIR(integer,integer)) then if type(b,PAIR(integer,integer)) then PAIR( op(1,a) + op(1,b), op(2,a) + op(2,b) ); else PAIR( op(1,a) + b, op(2,a) + b ); end if; elif type(b,PAIR(integer,integer)) then PAIR( a + op(1,b), a + op(2,b) ); else error("invalid input: a or b should be a PAIR structure"); end if; end proc; end module; with(PairMath); 1 + PAIR(2,3); 2 + 2;
PAIR⁡3,4
Another option is to use the overload function to achieve polymorphism.
PairMath := module() option package; export `+`; local PP, PA, AP; PP := proc( a::PAIR(integer,integer), b::PAIR(integer,integer) ) option overload; print("in PP"); PAIR( op(1,a) + op(1,b), op(2,a) + op(2,b) ); end proc; PA := proc( a::PAIR(integer,integer), b ) option overload; print("in PA"); PAIR( op(1,a) + b, op(2,a) + b ); end proc; AP := proc( a, b::PAIR(integer,integer) ) option overload; print("in AP"); PAIR( a + op(1,b), a + op(2,b) ); end proc; `+` := overload( [ PP, PA, AP ] ); end module; with(PairMath); 1 + PAIR(2,3); PAIR(2,3) + 4; PAIR(1,1) + PAIR(3,4); 5+5;
in AP
in PA
in PP
10
For more information, see the overload help page.
8.7 Interfaces and Implementations
Generic programming is a programming style and a software engineering methodology for writing reusable code. Many Maple built-in operations are generic, for example, the addition operator + computes sums of integers, rational numbers, complex numbers, polynomials, special functions, and so on. When using the addition operator +, you do not need to define how an expression is represented-- the automatic simplifier recognizes how Maple expressions are represented. As with any dynamically typed language, Maple allows for generic programming. Most built-in Maple operations (including many standard library commands) are naturally polymorphic in that they can perform successfully with many data formats.
Generic Programming as a Good Software Engineering Practice
When working on any large project, it is important to write reusable code; that is, code that can perform a well-defined function in a variety of situations. Generic programs do not rely on the details of how their inputs are represented. They can perform their function on any inputs that satisfy a specified set of constraints. Normally, these constraints are described in terms of the behavior of the inputs rather than on their physical representation or the storage layout of their concrete representation. This behavior is sometimes called a contract. Generic programs rely only on the object behavior specified by the contract. They do not rely on information of how an object is implemented; therefore, generic programs separate interfaces from implementations.
Distinction between Local and Exported Variables
The behavior specified by the contract for a module includes any module exports. Whatever is expressed through its local variables is private to the module, and is not to be relied on, or even known, by clients of the module. (Client access is, in fact, the only technical difference between module locals and exports.)
Before the introduction of the module system, design by contract was enforced in Maple only by convention. Maple commands whose names had to be enclosed in name quotes (`) were considered private, and not for client use. However, this was only a convention. Also, it was necessary to use global variables to communicate information and state among the commands that comprised a subsystem (such as solve or assume). Now, using modules, it is possible to design software systems that enforce their contracts by a mechanism embedded in the Maple language.
Interfaces
In Maple, contracts are represented by an interface, which is a special kind of structured type. It has the form
`module`( symseq );
where symseq is a sequence of symbols or expressions of the form symbol::type. For example, an interface for a ring can be written as
`type/ring` := '`module`( `+`, `*`, `-`, zero, one )':
while an (additive) abelian group can take the form
`type/abgroup` := '`module`( `+`, `-`, zero )':
These symbols are the ones to which clients have access as module exports.
A module is said to satisfy, or to implement, an interface if it is of the type defined by the interface.
z5 := module() description "the integers modulo 5"; export `+`, `*`, `-`, zero, one; `+` := (a,b) -> a+b mod 5; `*` := (a,b) -> a*b mod 5; `-` := s -> 5-s mod 5; zero := 0; one := 1; end module:
type( z5, 'ring' );
A module can satisfy more than one interface.
type( z5, 'abgroup' );
Interfaces are an abstraction that form part of the Maple type system. They provide a form of constrained polymorphism. Not every Maple type is an interface; only those that have the form described are interfaces. You can define a Maple type (that, as it happens, is not itself an interface) to describe interfaces.
`type/interface` := 'specfunc( {symbol,symbol::type}, `module` )':
This is a structured type. It describes expressions that are themselves structured types. They have the form of an unevaluated function call with the operator symbol `module` and all arguments of type symbol, or of type symbol::type. In the two previous examples in this section, the types type/ring and type/abgroup are the interface expressions, and the names ring and abgroup are the respective names of those interfaces.
A Package for Manipulating Interfaces
The following example illustrates a package for manipulating interfaces. The package is small enough that it can be included here, in full, but it is also available in the samples/ProgrammingGuide directory of your Maple installation.
Interface := module() description "a package for manipulating interfaces"; global `type/interface`; export define, # define an interface extend, # extend an interface extends, # test for an extension equivalent,# test equivalence savelib, # save an interface satisfies; # test whether a module satisfies # an interface local gassign, # assign to a global variable totype, # convert from interface name to type toset, # convert from interface name to a set setup; # install `type/interface` globally option package, load = setup; # Define a global type for interfaces. # This assignment takes care of installing the type # in the Maple session in which this module definition # is evaluated. Calling `setup()' ensures that this also # happens when the instantiated module is read from a # Maple library archive. `type/interface` := 'specfunc( {symbol, `::`}, `module` )'; # Ensure that `type/interface` is defined. This thunk is # called when the instantiated `Interface' module is read # from a Maple library archive. setup := proc() global `type/interface`; `type/interface` := 'specfunc( {symbol, `::`}, `module` )'; NULL # quiet return end proc; # Assign to the global instance of a name gassign := proc( nom::symbol, val ) option inline; eval( subs( _X = nom, proc() global _X; _X := val end proc ) )() end proc; # Convert an interface name to the corresponding type. totype := ( ifc::symbol ) -> ( `type/` || ifc ); # Convert an interface name to a set of symbols. toset := ( ifc::symbol ) -> { op( ( `type/` || ifc ) ) }; # Install a new interface into the type system. define := proc( ifc ) description "define an interface"; if map( type, {args}, 'symbol' ) <> { true } then error "arguments must all be symbols" end if; gassign( `type/` || ifc, '`module`'( args[ 2 .. nargs ] ) ); ifc # return the interface name end proc; # Implement subtyping. extend := proc( new, old ) description "extend an existing interface"; if map( type, {args}, 'symbol' ) <> { true } then error "arguments must all be symbols" end if; if not type( totype( old ), 'interface' ) then error "cannot find an interface named %1", old end if; define( new, op( totype( old ) ), args[3..nargs] ) end proc; # Test whether ifc2 is an extension of ifc1. extends := proc( ifc1, ifc2 ) description "test whether the second interface " "extends the first"; local t1, t2; t1, t2 := op( map( totype, [ ifc1, ifc2 ] ) ); if not type( [t1,t2], '[interface,interface]' ) then if not type( t1, 'interface' ) then error "arguments must be interface names, " "but got %1", ifc1 else error "arguments must be interface names, " "but got %1", ifc2 end if end if; toset( ifc1 ) subset toset( ifc2 ) end proc; # Save an interface to the Maple library archive. savelib := proc() description "save a named interface to a " "Maple library archive"; local ifc; for ifc in map( totype, [ args ] ) do if not type( ifc, 'interface' ) then error "arguments must be interfaces, " "but got %1", ifc end if; :-savelib( totype( ifc ) ) end do end proc; # Test whether a module satisfies an interface. # This is simply an alternative to a call # to `type()'. satisfies := proc( m, ifc ) description "test whether a module satisfies an interface"; if not type( totype( ifc ), 'interface' ) then error "second argument must be an interface name, " "but got %1", ifc end if; type( m, ifc ) end proc; # Test whether two interfaces are equivalent. # Since unevaluated function calls compare # differently if their arguments are in a # different order, we convert them to sets first, # and then test for equality. equivalent := proc( ifc1, ifc2 ) description "test whether two interfaces " "are equivalent"; local t1, t2; t1, t2 := totype( ifc1 ), totype( ifc2 ); if not type( t1, 'interface' ) then error "expecting an interface name, " "but got %1", ifc1 elif not type( t2, 'interface' ) then error "expecting an interface name, " "but got %1", ifc2 end if; evalb( { op( t1 ) } = { op( t2 ) } ) end proc; end module:
This package implements the interface abstraction. It allows you to manipulate interfaces without having to consider how they fit into the Maple type system.
with( Interface );
define,equivalent,extend,extends,satisfies,savelib
define( 'abgroup', '`+`', '`-`', 'zero' );
abgroup
type( `type/abgroup`, 'interface' );
satisfies( z5, 'abgroup' );
extend( 'ring', 'abgroup', '`*`', 'one' );
ring
type( `type/ring`, 'interface' );
extends( abgroup, ring );
satisfies( z5, 'ring' );
The load Option
This package provides an abstraction of the interface concept in Maple and illustrates a module feature that was not previously demonstrated: the load=procedure_name option. In the Interface package, this option is used in a typical way. The declaration
option load = setup;
that appears in the module definition indicates that, when the instantiated module is read from a Maple library archive, the procedure setup is to be called. The procedure named must be a local variable or an exported local variable of the module. The local procedure setup in this module simply ensures that the global variable type/interface is assigned an appropriate value. This assignment is also made in the body of the module so that the assignment is also executed in the session in which the module is instantiated. This is done for illustrative purposes. A better approach would be to invoke setup in the body of the module definition.
Download Help Document