Contents Previous Next Index
11 Writing Packages
This section describes how to collect a large software project in Maple into a package that is easy to maintain. Packages can be configured to load automatically when you start Maple and distributed to other users as a library rather than as Maple source code.
11.1 In This Chapter
What is a package
Writing Maple packages by using modules
Examples of custom packages
11.2 What Is a Package
A package is a collection of procedures and other data that can be treated as a whole. Packages typically gather a number of procedures that enable you to perform computations in a well-defined problem domain. Packages may contain data other than procedures, and may even contain other packages (subpackages).
Packages in the Standard Library
A number of packages are shipped with the standard Maple library. For example, the GroupTheory, NumberTheory, CodeGeneration, and LinearAlgebra packages are all provided with Maple, along with several dozen others. The GroupTheory package provides procedures for computing with groups that have a finite representation in terms of permutations, or of generators and defining relations. The LinearAlgebra package provides numerous procedures for computational linear algebra.
Packages Are Modules
Modules are the implementation vehicle for packages. A module represents a package by its exported names. The exported names can be assigned arbitrary Maple expressions, typically procedures, and these names form the package.
For more information about modules, see Programming with Modules.
Some older and deprecated Maple packages such as simplex and networks are not implemented using modules; they are implemented using tables. In table-based packages, the name of a package command is used as the index into a table of procedures. It is not recommended to write new packages using tables since modules allow much more flexibility.
Package Exports
Some of the data in a package is normally made accessible to the user as an export of the package. For packages implemented as modules, the package exports are the same as the exports of the underlying module. For packages implemented as tables, the package exports are the names used to index the underlying table.
Accessing the exports of a package is a fundamental operation that is supported by all packages. If P is a Maple package, and e is one of its exports, you can access e by using the fully qualified reference P[ e ]. If P is a module, you can also use the syntax P:-e. These methods of accessing the exports of a module are normally used when programming with a package.
Note that the member selection operator (:-) is left-associative. If S is a submodule of a module P, and the name e is exported by S, then the notation P:-S:-e is parsed as (P:-S):-e, and so it refers to the instance of e, which is local to S. This concept is important for referencing members of subpackages. For example,
CodeTools:-Profiling:-Coverage:-Print();
calls the procedure Print in the subpackage Coverage in the subpackage Profiling, which is part of the CodeTools package. You can use indexed notation for this.
CodeTools[Profiling][Coverage][Print]();
Using Packages Interactively
For interactive use, it is inconvenient to enter fully qualified references to all of the exports of a package. To facilitate the process of entering package command names, the Maple procedure with is provided for the interactive management of package namespaces. By using with, you can globally impose the exported names of a package. This allows you to access the package exports, without typing the package prefix, by making the names of the exports accessible at the top level of the Maple session. For example, to use the NumberTheory package, enter the command
with( NumberTheory );
AreCoprime,CalkinWilfSequence,CarmichaelLambda,ChineseRemainder,ContinuedFraction,ContinuedFractionPolynomial,CyclotomicPolynomial,Divisors,FactorNormEuclidean,HomogeneousDiophantine,ImaginaryUnit,InhomogeneousDiophantine,IntegralBasis,InverseTotient,IsCyclotomicPolynomial,IsMersenne,IsSquareFree,IthFermat,IthMersenne,JacobiSymbol,JordanTotient,KroneckerSymbol,Landau,LargestNthPower,LegendreSymbol,Möbius,ModExtendedGCD,ModularLog,ModularRoot,ModularSquareRoot,Moebius,MultiplicativeOrder,Möbius,NearestLatticePoint,NextSafePrime,NumberOfIrreduciblePolynomials,NumberOfPrimeFactors,Ω,Φ,PrimeCounting,PrimeFactors,PrimitiveRoot,PseudoPrimitiveRoot,QuadraticResidue,Radical,RepeatingDecimal,RootsOfUnity,SimplestRational,SumOfDivisors,SumOfSquares,ThueSolve,Totient,λ,μ,φ,π,σ,τ,ϕ
This command makes the names exported by the NumberTheory package (a list of which is returned by the call to with) available temporarily as top-level Maple commands.
Divisors( 60 );
1,2,3,4,5,6,10,12,15,20,30,60
11.3 Writing Maple Packages By Using Modules
A Simple Example
The simplest type of package is a collection of related procedures that are bundled together for convenience, for example, PolynomialTools or ArrayTools are packages which are both included with Maple. The following is an example of a custom package.
SomeTools := module() description "Some useful tools"; option package; export axpy, sqrm1, identity; axpy := proc(a::algebraic, x::algebraic, y::algebraic, $) description "compute a times x plus y"; return a*x + y; end proc; # axpy identity := proc() description "return the arguments"; return _passed; end proc; # axpy sqrm1 := proc(x::algebraic, $) description "square minus one"; return x^2 - 1; end proc; end module; # Some Tools
SomeTools≔module...end module
This example is simply a module that consists of a few exported members that are procedures and an option called package. As with all modules, its members can be accessed by using the :- and [] operators.
SomeTools:-axpy(1,1,1);
2
SomeTools[sqrm1](2);
3
The package option also allows you to call the package by using the with command to access the package exports.
with(SomeTools);
axpy,identity,sqrm1
identity("nothing");
nothing
If the with command is used to call a module that does not include the package option, an exception will occur:
SomeOtherTools := module() export Nothing; Nothing := x->x; end module;
SomeOtherTools≔module...end module
with(SomeOtherTools);
Warning, SomeOtherTools is not a correctly formed package - option `package' is missing
Nothing
Packages generally include many lines of Maple code, so you will probably want to create and modify them in a specialized editor designed for programming such as vim or emacs. In the example above, the definition of the module SomeTools have been put in a file called SomeTools.mpl in the samples/ProgrammingGuide/ directory of your Maple installation). If you copy this file into the current directory, it can then be loaded in Maple by using the read command.
read("SomeTools.mpl");
This allows you to access the SomeTools package commands in a Maple worksheet or document. You can include a read statement as the first executed command or in the startup code of a Maple worksheet or document.
Custom Libraries
If you prefer not to call the read command to load your custom package, you can save your package as a Maple library archive (.mla) file. This allows your package to be available whenever Maple is started; however, it is loaded into memory only if it is required. In contrast, the read method automatically loads the package into memory.
Before a package can be saved to a library archive, it must be loaded into Maple, either directly in a worksheet or by using the read command. Then, the simplest method for saving it to a library archive is to call the savelib command.
Before calling the savelib command, you may want to set up a directory to store your custom library. For example, you can create a directory called maple in your home directory and use the LibraryTools commands to create an empty .mla file in which to store your library.
mylibdir := cat(kernelopts(homedir), kernelopts(dirsep), "maple", kernelopts(dirsep), "toolbox", kernelopts(dirsep), "personal", kernelopts(dirsep), "lib");
C:\Documents and Settings\juser\maple\toolbox\personal\lib
FileTools:-MakeDirectory(mylibdir, 'recurse');
LibraryTools:-Create(cat(mylibdir, kernelopts(dirsep), "packages.mla"));
libname := mylibdir, libname;
libname ≔ C:\Documents and Settings\juser\maple\toolbox\personal\lib,C:\Program Files\Maple 15\lib
To save a new value for libname (a predefined variable, which specifies the location of the main Maple library, and package and user libraries), and to make sure that this directory is the default location for saving in the future after you use the restart command, add the line above to your Maple initialization file, which specifies initialization settings for Maple.
For more information on initialization files, see the worksheet/reference/initialization help page.
Note: Maple automatically adds lib subdirectories of directories in HOMEDIR/maple/toolbox to the predefined variable libname. Therefore, modifying the .mapleinit or maple.ini file is only necessary if you want to designate a directory as the default location in which the savelib command will save your library files.
You can modify the Maple initialization file manually in a text editor or by using the FileTools commands in Maple:
mapleinitfile := cat(kernelopts(homedir), kernelopts(dirsep), `if`(kernelopts(platform)="unix", ".mapleinit", "maple.ini"));
C:\Documents and Settings\juser\maple.ini
FileTools:-Copy(mapleinitfile, cat(mapleinitfile, ".mpl.bak"));
FileTools:-Text:-Open( mapleinitfile, 'append');
FileTools:-Text:-WriteLine(mapleinitfile, cat("libname := \"",mylibdir,"\", libname:"));
FileTools:-Text:-Close( mapleinitfile );
Restart, and verify that your libname is set now correctly.
restart;
libname;
Finally, save the package to the library archive by calling the savelib command.
savelib( 'SomeTools' );
Enter the restart command, followed by the ShowContents command in LibraryTools to verify that the package has been added to the library archive. If everything has worked correctly, you can now use the with command to access the package commands.
LibraryTools:-ShowContents(libname[1]);
Running the savelib command saves the module to the first library archive found in the path specified by the global variable libname or the library archive in the path specified by the global variable savelibname, if it is defined. (At least one of these values must be defined.) You can save your package to a different library archive by using the LibraryTools:-Save command or by providing a file name as a second argument to the savelib command.
You will want to remove this example package from your library when you are done. This can be done using the Delete command in the LibraryTools package.
LibraryTools:-Delete('SomeTools', libname[1]);
You can confirm that it has been deleted.
Important: Always make sure that the standard Maple library directory is write-protected to avoid saving expressions in it. If you accidentally save a file to the standard Maple library, you may need to reinstall Maple to restore the main library archive.
11.4 A Larger Example
Several additional techniques are useful for writing larger packages. Some of these techniques will be described in the context of an example package called RandomnessTests whose full source can be found in the samples/ProgrammingGuide/RandomnessTests directory, which is located in the directory where Maple is installed. It is a package containing procedures to analyze the randomness of a sequence of bits.
ModuleLoad
Often, a package needs to initialize the internal or global state when it is loaded. For example, many packages define new types for the type system. Generally, these types are not needed unless the package is loaded, and so they are created by the ModuleLoad local member of the package. In this example, we will define a type BinarySequence that is a linear data type containing only zeros and ones.
ModuleLoad := proc() TypeTools:-AddType ( ':-BinarySequence', proc(L) type(L, list({identical(0),identical(1)})) or type(L, 'Vector'({identical(0),identical(1)})) or ( type(L, 'Array'({identical(0),identical(1)})) and nops([rtable_dims(L)])=1 and op([1,1], [rtable_dims(L)])=1 ); end proc ); end proc;
The local named ModuleLoad will automatically run when a module is loaded from a library. If you are using the read command to load the definition of the module into Maple or if you enter it in a worksheet, this procedure will not run automatically.
You can also define the local ModuleUnload, which will run if the module is removed from memory because it is not being used. In this case, the custom type definition BinarySequence is removed.
ModuleUnload := proc() TypeTools:-RemoveType(':-BinarySequence'); end proc;
It is also possible to use the load=proc and unload=proc options in the module definition to specify different procedures to be invoked when a package is loaded and unloaded respectively. However, the use of the ModuleLoad and ModuleUnload procedures is recommended.
The Preprocessor and Structured Source Files
If a package has many exports, it often useful to put each export into its own source file. The Maple preprocessor is similar to the preprocessor of a C compiler. It allows you to specify the names of source files and macro definitions to include in a master .mpl file, which defines the contents of your package.
To include the file name of an export in an .mpl file, you can use the $include directive.
To define macros, you can use the $define directive.
In our example, we have a file called RandomnessTests.mpl, which has several exports stored in the same directory in the files: WaldWolfowitz.mm, BitFrequency.mm, Compressibility.mm, BinaryRank.mm. The following example shows the contents of the RandomnessTests.mpl file.
##MODULE RandomnessTests ## ##DESCRIPTION ##- A package containing commands for testing the randomness of ## binary sequences. $define RANDINFO ':-RandomnessTests' $define MAINLEVEL 2 RandomnessTests := module() option package; export WaldWolfowitz, BitFrequency, Compressibility, BinaryRank; local Runs, ModuleLoad, ModuleUnload; ModuleLoad := proc() TypeTools:-AddType ( ':-BinarySequence', proc(L) type(L, list({identical(0),identical(1)})) or type(L, 'Vector'({identical(0),identical(1)})) or ( type(L, 'Array'({identical(0),identical(1)})) and nops([rtable_dims(L)])=1 and op([1,1], [rtable_dims(L)])=1 ); end proc ); end proc; ModuleUnload := proc() TypeTools:-RemoveType(':-BinarySequence'); end proc; $include "WaldWolfowitz.mm" $include "BitFrequency.mm" $include "Compressibility.mm" $include "BinaryRank.mm" end module; # RandomnessTests
When this file is loaded into Maple from the command-line interface or by using the read command in a Maple worksheet or document, Maple automatically replaces each $include directive with the contents of the file specified.
The file also includes two $define directives which are macros that are not used in this file, but will be used in the included files. Including them in the top-level source file allows us to make package-wide changes by editing the macros in one place. In our example, these macros will be used to control the userinfo definitions throughout the package. The following line appears in the WaldWolfowitz.mm file:
userinfo(MAINLEVEL, RANDINFO, nprintf("sequence has %d runs", runs));
When loaded, the preprocessor will transform this line into the following:
userinfo(2, ':-RandomnessTests', nprintf("sequence has %d runs", runs));
Macros can also have parameters such as the following macro in the BinaryRank.mm file:
$define GF2RankProbability(m,n,r) 2^(r*(n+m-r)-m*n)*mul(((1-2^(l-n))*(1-2^(l-m)))/(1-2^(l-r)),l=0..(r-1))
which allows GF2RankProbability to be used as if it were a procedure. However, it will be replaced inline by the preprocessor. This is similar to how procedures with option inline function, but with more restrictions.
Subpackages
When creating large packages, it is useful to organize commands into smaller subpackages. Our package does this with a Visualization subpackage and a Data submodule to store sample random inputs.
Achieving this is as simple as including the definition for these other modules and packages within the top-level package.
11.5 Example: A Shapes Package
In this section, a sample package is presented to illustrate concepts that may be helpful when working with modules and submodules, and putting them together into a package.
Modules allow you to create packages with a hierarchical structure; this cannot be done with table-based implementations of packages. This section covers the following topics:
Organizing the source code for a large package that has a nontrivial substructure.
A description of the Shapes package, including details of its design and implementation
Hints related to source code organization.
The package presented in this section provides the means to compute areas and circumferences of various planar figures, which are called shapes.
Note: Only portions of the source code for this package are shown. The fully commented source code can be found in the samples/ProgrammingGuide/shapes directory of your Maple installation.
Source Code Organization
The Shapes package is organized into several source files:
shapes.mpl
point.mm
segment.mm
circle.mm
square.mm
triangle.mm
To avoid platform-specific differences, all of the source files are located in the same directory or folder.
Figure 11.1: Organization of Package Source Files
To define the module that implements this package, use the Maple preprocessor to include the remaining source files at the appropriate point in the master source file shapes.mpl. A number of $include directives are included in shapes.mpl, such as
$include "point.mm" $include "segment.mm"
Splitting a large project into several source files makes it easier to manage and allows several developers to work on a project at the same time. The source file is divided into shape-specific functionality. Most of the functionality for points, for instance, is implemented by the source code stored in the point.mm file.
Package Architecture
The Shapes package is structured as a module with several exported procedures. Individual submodules provide specific functionality for each shape type supported by the package. Each of these shape-specific submodules is stored in its own source file; these files are included into the main package source file, shapes.mpl.
The package module Shapes has a submodule, which is also called Shapes. The submodule Shapes:-Shapes contains one submodule for each shape supported. The submodule hierarchy is illustrated in Figure 11.2.
Figure 11.2: Design of Package
The result of preprocessing the main file shapes.mpl produces a module whose source has the following general form.
Shapes := module() option package; export make, area, circumference; local Shapes, circum_table; Shapes := module() export point, segment, circle, square, triangle; point := module() ... end module; segment := module() ... end module; ..... end module; make := proc() ... end proc; area := proc() ... end proc; circum_table := table(); ... circumference := proc() ... end proc; end module:
The Package API
The Shapes package exports the following procedures:
make
area
circumference
The make Procedure
The exported procedure make creates shapes. It is used to create a shape expression from the input data. For example, points are derived from their x and y coordinates.
org := make( 'point', 0, 0 );
org≔make⁡point,0,0
A circle is created from its center and radius.
circ := make( 'circle', org, 2 );
circ≔make⁡circle,make⁡point,0,0,2
In each case, the name of the shape is passed as the first argument to specify which kind of shape to return.
The area Procedure
To compute the area of a shape, call the exported procedure area with the shape as its argument.
area( circ );
area⁡make⁡circle,make⁡point,0,0,2
The circumference Procedure
The exported procedure circumference computes the circumference of a given shape.
circumference( circ );
circumference⁡make⁡circle,make⁡point,0,0,2
Shape Representation
Shapes are represented as unevaluated function calls. The arguments to the call are the instance-specific data for the shape. For example, a point with coordinates (2,3) is represented by the unevaluated function call POINT( 2, 3 ). Some instance data are shapes themselves. For example, a segment is represented, using its endpoints, as an unevaluated function call of the form SEGMENT( start_point, end_point ). The start and end points of the segment can be obtained by calling the point constructor.
Procedure Dispatching
The Shapes package illustrates three types of procedure dispatching.
Dispatching on submodule exports
Conditional dispatching
Table-based dispatching
Dispatching on Submodule Exports
The make procedure, which is exported from the Shapes package, uses the Shapes:-Shapes submodule for procedure dispatching.
To test whether a method for a given shape is available, the make procedure tests whether there is a submodule by that name in the Shapes:-Shapes submodule. If no such submodule is found, an exception is raised. Otherwise, the make export from the submodule is passed the arguments that were given to the top-level Shapes:-make procedure. The make source code is as follows.
make := proc( what::symbol ) description "constructor for shapes"; local ctor, # the shape constructor, # if found theShape; # the submodule for the # kind of shape requested if not member( what, Shapes, 'theShape' ) then error "shape `%1' not available", what end if; if member( ':-make', theShape, 'ctor' ) then ctor( args[ 2 .. nargs ] ) else error "no constructor provided for " "shape %1", what end if end proc:
The first argument to make is a symbol that specifies the shape to create (point, circle, triangle). This symbol is used as an index in the Shapes:-Shapes submodule. The first statement uses the member command to test whether the symbol passed in the what parameter is exported by the Shapes:-Shapes submodule. If it is not found, an appropriate diagnostic is issued and an exception raised. If member returns the value true, then its third argument, the local variable theShape, is assigned the export found in the submodule.
For example, if what is the symbol circle, then the local variable theShape is assigned the submodule Shapes:-Shapes:-circle that implements operations on circles. The same idea is used to select the shape-specific constructor; it is the value assigned to the local variable ctor when the value true is returned from the second call to the member command. Any remaining arguments are used as data to construct the shape. These are passed to the make export in a shape-specific submodule, if found, and are not checked further at this level. This design localizes the shapes to the corresponding submodule.
Conditional Dispatching
The procedure area uses a simple conditional dispatching mechanism. The tag of the input shape is extracted and is used in direct comparisons with hard-coded values to determine which shape-specific area subcommand to call to perform the area computation.
area := proc( shape ) description "compute the area of a shape"; local tag; if not type( shape, 'function' ) then error "expecting a shape expression, " "but got %1", shape end if; # Extract the "tag" information from the shape tag := op( 0, shape ); # Dispatch on the "tag" value if tag = ':-POINT' then Shapes:-point:-area( shape ) elif tag = ':-SEGMENT' then Shapes:-segment:-area( shape ) elif tag = ':-CIRCLE' then Shapes:-circle:-area( shape ) elif tag = ':-SQUARE' then Shapes:-square:-area( shape ) elif tag = ':-TRIANGLE' then Shapes:-triangle:-area( shape ) else error "not a recognized shape: %1", tag end if end proc:
Table-based Dispatching
The third dispatch method illustrated in the Shapes package is table-based. This technique is used by the exported procedure circumference, which references the table circum_table to look up the appropriate procedure to call. This table is built by assigning its entries in the body of the Shapes package.
circum_table := table();
circum_table[ 'POINT' ] := Shapes:-point:-circumference;
circum_table[ 'SEGMENT' ] := Shapes:-segment:-circumference;
circum_table[ 'CIRCLE' ] := Shapes:-circle:-circumference;
circum_table[ 'SQUARE' ] := Shapes:-square:-circumference;
circum_table[ 'TRIANGLE' ] := Shapes:-triangle:-circumference;
The source code for the procedure circumference is as follows.
circumference := proc( shape ) description "compute the circumference of a " "shape expression"; if not type( shape, 'function' ) then error "expecting a shape, but got %1", shape end if; if assigned( circum_table[ op( 0, shape ) ] ) then circum_table[ op( 0, shape ) ]( shape ) else error "no circumference method available " "for shape %1. Supported shapes " "are: %2", tag, sprintf( "%q", op( ALL_SHAPES ) ) end if end proc:
Minimal checking is done to ensure that the input has the right structure. If an entry is found in the table circum_table for the shape tag (as with the area procedure), the corresponding procedure is called with the given shape as an argument. (The shape must be passed as an argument, so that the shape-specific submodule can extract the instance data from it.) Otherwise, a diagnostic is issued and an exception is raised.
Shape-specific Submodules
As already noted, each shape is implemented in a shape-specific submodule. The set of exports of each module varies, but each supports the required exports make, area, and circumference in the top-level Shapes module. Certain shapes support other operations. Only two submodules are described here. You can see the source for the other submodules in the sample source code.
The point Submodule
The submodule that implements points is fairly simple. In fact, it makes no reference to any lexically scoped variables in its parent modules (Shapes and Shapes:-Shapes).
point := module() description "support commands for points"; export make, area, circumference, xcoord, ycoord; option package; make := ( x, y ) -> 'POINT'( x, y ); area := () -> 0; circumference := () -> 0; xcoord := p -> op( 1, p ); ycoord := p -> op( 2, p ); end module:
Since the area and circumference of a point are both 0, these procedures are easy to implement. In addition to the required exports, the point submodule also exports two utility procedures, xcoord and ycoord, for retrieving the x and y coordinates of a point. Providing these values makes it possible for clients of this submodule to use it without requiring information about the concrete representation of points. This makes it easier to change the representation later, if required.
Within this submodule, the names make, area, and circumference are the same as the names with the same external representation at the top-level Shapes module.
The circle Submodule
This submodule provides the circle-specific commands for the Shapes package.
circle := module() export make, center, radius, diameter, area, circumference; option package; make := proc( cntrPt, radius ) 'CIRCLE'( cntrPt, radius ) end proc; center := circ -> op( 1, circ ); radius := circ -> op( 2, circ ); diameter := circ -> 2 * radius( circ ); circumference := circ -> Pi * diameter( circ ); area := circ -> Pi * radius( circ )^2; end module:
Again, some extra commands are provided in addition to those required at the top level of the Shapes package. The exported procedure radius is used to define other commands. It can be made local to this submodule.
Download Help Document