Writing Packages

from Essential Mathematica for Students of Science
© James J. Kelly, 1998
Last revised: January, 2006

Introduction

You have by now acquired considerable experience in writing modular Mathematica programs and have probably written many small programs that would be useful in your major subject of study.  While you could copy these functions from one notebook to another, having multiple copies can lead to headaches.  You probably want improvements in such functions to be implemented in any notebook that uses them, which is difficult when there are many copies evolving independently.  Furthermore, the symbols you define in your current notebook might interfere with those used in the functions that you want to import.  Obviously the solution is to collect your most useful functions into packages of related Mathematica code that can be loaded any time that it is needed, with the details safely hidden.  Thus, evolution is localized and its benefits shared.  As you know, Mathematica itself employs the same strategy of packaging its code so that built-in functions evolve uniformly as new capabilities are introduced.

This lesson will introduce the basic techniques for writing packages consistent with normal Mathematica conventions.  To illustrate the method, we will develop a prototype package Fractals.m that includes the functions for the Mandelbrot set, Julia sets, and Koch curves that we wrote earlier this semester.  Although we have become accustomed to developing Mathematica code using a notebook interface with extensive typesetting and formatting features, packages must be written in ordinary ascii text using only ordinary keyboard characters.  Therefore, we must convert the code from notebook form to package form.  Unfortunately, because Mathematica does not yet provide effective conversion tools, we must either rewrite the code using an external editor or must employ the somewhat cumbersome procedure outlined below.  If you follow these instructions carefully, you should encounter little difficulty converting between notebook and package formats.

Preparing a package file

Step 1: open a fresh notebook

Open a new notebook using the default style sheet.  As you enter statements into the fledgeling package, please observe the following rule: only one complete statement per cell!  When copying cells from other notebooks into the new package, you must split any cell containing several statements so that there is only one statement in each cell.  Embedded comments will be removed by the conversion process, but can be replaced later if desired.

Step 2: prepare skeleton

Every Mathematica package has the same basic format.  Enter the following statements, each in its own cell.  Note that we have disabled the evaluation of the following cells.

BeginPackage["Fractals`"];

Begin["`Private`"];

End[];

Protect[];

EndPackage[]

Step 3: add flesh

Open functions.nb and copy the usage message for mandelbrotRate to the fledgeling package under the "usage messages" comment.  Then copy the function definition into the "definitions" area.  Each should occupy a single cell.

Open Julia.nb and copy the usage message for juliaRate to the fledgeling package under the "usage messages" comment.  Then copy the function definition into the "definitions" area.  Each should occupy a single cell.

Open Koch.nb and copy the usage messages for DisplayKoch and its options to the fledgeling package under the "usage messages" comment.  Then copy all necessary function definitions into the "definitions" area.  Don't forget to include the definitions for all functions used by DisplayKoch.  Each should occupy a single cell.  Finally, copy the options statement into the appropriate area.

Next write usage messages for any symbols exported by the functions in this package.  The present example does not actually require this step, but many packages would.

Enter Needs["Utilities`FilterOptions`"] under the "load required packages" comment.

Insert juliaRate, mandelbrotRate, DisplayKoch as arguments of Protect[].

Step 4: formatting

Packages must be written in the format for Mathematica source code using only ordinary characters found on the keyboard, but the cells that you copied into your fledgeling package probably include typesetting and format information that is designed for display by a notebook interface.

Select the notebook containing the fledgeling package.  Then remove all output cells using Kernel ↦ Delete All Output from the menu bar.  Answer yes.

Select each input cell in turn and convert it to input form either using Cell ↦ ConvertTo ↦ InputForm from the menu bar or Shift Control I from the keyboard.  Do not try this on comment cells — this operation removes comment lines and converts cells containing only comments to null input.

Save this notebook as a notebook file FractalsPrototype.nb.

Notice that two-dimensional symbols have been converted to longer character sequences, such as \!\(z\^2\) for (z^2).  Similarly, special characters like -> are converted to ascii text as well.  Although we could have prepared the code using this type of input, the notebook interface provides much more appealing input and display capabilities.

Step 5: does it work?

Get a fresh start by quitting the local kernel.  Then restart the kernel using Kernel ↦ Start ↦ Local without executing any other Mathematica commands.

Start another new notebook.

Check that Fractals does not appear on the context path by executing Contexts[] or Contexts["*rac*"] in your new blank notebook.

Evaluate the notebook containing your fledgeling package.  This can be done using Kernel ↦ Evaluation  ↦ Evaluate Notebook.  Don't worry about warning messages for spelling.

Check that Fractals now appears on the context path.  Then execute ?DisplayKoch, read the instructions, and execute DisplayKoch[{{0.,0.},{1.,0.}},4].

If your package is missing essential definitions, copy those into the notebook, quit the kernel, re-evaluate the package, and try again.

Update FractalsPrototype.nb.

Step 6: saving the package file

Assuming that the tests are successful, you are now ready to save the package in a file with the appropriate format.  Unfortunately,  File ↦ Save As Special ↦ Package Format does not seem to work as you might hope.  When I try this method, each statement is enclosed by comment characters (* statement *) which must then be removed using an external editor.  I recommend using File ↦ Save As Special ↦ Text instead.

Select the notebook containing the fledgeling package.  Then remove all output cells using Kernel ↦ Delete All Output from the menu bar.  Answer yes.

Remove In/Out Names by toggling Kernel ↦ Show In/Out Names from the menu bar.

Save the package file using File ↦ Save As Special ↦ Text with the name Fractals.m.  You may need to edit the file name externally to ensure that it begins with an uppercase character.

Using a simple external editor, such as Notepad for Windows, insert strategically placed blank lines, tabs, and spaces to enhance readability.  Replace embedded comments, if desired.

Once again quit and restart the kernel.

Set the current working directory using SetDirectory[folder] where folder is the name of the folder containing your package.  Verify that the file Fractals.m is seen in this folder using FileNames[].

Execute <<Fractals.m in your scratch notebook and verify that Fractals is on the context path.  Repeat the tests from preceding section.

Congratulations! You have successfully written your first package.

We will polish the notebook shortly, but first we discuss how the packaging process works.

The madness behind the method

The specialized commands one finds in packages are designed to manipulate contexts so that symbols that are intended for external use are associated with the Package` context while private symbols that are used by the implementation but are of no concern to the user are placed on the normally invisible context Package`Private`.   For the present example, we expect to find juliaRate, mandelbrotRate, and DisplayKoch in the Fractals` context.  Check by evaluating Context[juliaRate], etc.

Once again quit and restart the kernel.  Let us return to the prototype notebook FractalsPrototype.nb.  Save this notebook with a new temporary name so that you can play with it freely.  Execute the following statements one at a time to study how the package works.  Remove the terminal semicolons to see the output produced by each statement.

Contexts[]

Prints all available contexts.  After restarting the kernel, you should see only those contexts that are established upon start-up.  In particular, Fractals should be absent from this list.

$ContextPath

Prints the current context path used for creating and interpreting symbol names.

Context[]

Prints the current context.  Unless specified otherwise, new symbols are always created in the current context.  Evaluation of $Context also reports the current context.

BeginPackage["Fractals`"]

Sets the current context to Fractals`  and modifies the context path — verify by executing $ContextPath and $Context again.  Next,

Needs["Utilities`FilterOptions`"]

adds Utilities`FilterOptions` to the context path without changing the current context (check!).

New symbols are then automatically created in the current context.  The usage messages for functions and their options are placed in this section of the package in order to establish symbols in the current context that can be exported to the user who loads the package.  Evaluate the cells containing the usage messages, then execute Context[juliaRate] to verify that juliaRate appears in the Fractals` context.

Begin["`Private`"]

Sets the current context to Fractals`Private`  — verify by executing $Context.  New symbols are now automatically created in this private context that is not intended to be visible to the casual user of the package.  In order to access these symbols, the user would have to invoke the full name including context.  Thus, this is the section where the detailed implementation is written hidden from direct view.  The symbols for which usage messages were defined in the export context, Fractals`, are available but the intermediate variables, such as newpt1, that the user does not need to see are not available without excavation (full context plus symbol).  Evaluate the definitions in this section.

End[]

Terminates the private context and resets the current context to Fractals` — verify by executing $Context.

Protect[symbol1, symbol2,  ... ]

Prevents the user from inadvertently modifying the definitions you have established for these symbols.

EndPackage[]

Resets the context and prepends Fractals` to the context path.  When you execute $ContextPath you should now find Fractals` followed by the original context path.  ?juliaRate should provide information about this exported symbol, while ?newpt1 should not recognize this hidden symbol.  However, nothing is truly hidden.  Executing

?Fractals`Private`newpt1

will reveal its definition.

Error messages

Functions defined in terms of appropriately designed patterns do not respond to input that does not match those patterns.  Hence, no result will be returned to a user who calls your function with arguments for which there are no definitions.  This is in fact the desired behavior — it is better to receive no result than to be misled by spurious results.  Nevertheless, it is usually desirable to provide informative error messages when blatantly incorrect input is encountered.  Furthermore, good functions also provide informative messages when an algorithm fails or when the input is numerically unsuitable despite matching the defined patterns.

Mathematica defines error messages using the form

Symbol::tag="informative text, perhaps with place holders `1`, `2`, ⋯ for insertion of values"

where Symbol identifies the symbol and tag the type of error encountered.  The error message is then printed by the statement

Message[Symbol::tag, value1, 
      value2, ⋯]

where the optional values are inserted into the place holders.

For example, DisplayKoch obviously cannot tolerate negative iteration numbers.  Therefore, add the following statement

DisplayKoch::negiter = "negative iteration number";

to the "error conditions" section of the package and the statement

If[n <= 0, Message[DisplayKoch::negiter]; 
        Abort[]];

near the beginning of the DisplayKoch module.  In order to test these messages, it is useful to temporarily disable the protection of the export functions by commenting out the Protect statement.   If you then execute the Unprotect function, re-evaluation of the notebook will establish the desired error messages.  However, do not forget to restore the Protect statement before you finish.

Now try computing a Koch curve with negative iteration index.  The condition in the If statement tests True and causes the error message to be printed and further evaluation of the function to be aborted.

Next, suppose that the user calls DisplayKoch with an inappropriate first argument, such as a number rather than a list of coordinate pairs.  Although no output would be produced because the input does not match the pattern specified by the definition, an informative message might be helpful.  Add the message

DisplayKoch::notpoly = "first argument should be a list of \
pairs";

and the definition

DisplayKoch[x_, n_, opts___] := 
        Message[DisplayKoch::notpoly];

to the "error conditions" section.  This secondary definition will not interfere with the primary definition because Mathematica searches the definition list for a matching pattern in the order specified by the package and for correct input will find a match at the primary definition first.  Thus, the secondary definitions in the "error conditions" section are examined only when inappropriate input is given.  Re-evaluate the package and test the functioning of this message.

Finally, suppose that the number of arguments is incorrect.  The statements

DisplayKoch::badarg = "`1` arguments given where 2 were \
expected";

DisplayKoch[x_] := 
        Message[DisplayKoch::badarg, 1];

will handle the situation in which too few arguments are given and can be generalized easily.

Add and test error messages for the Julia and Mandelbrot functions which handle several classes of inappropriate arguments (too many, too few, iteration limits not being positive integers, etc).

You have just discovered a problem with DisplayKoch — integer or rational coordinates burden the computation excessively.  Demonstrate and then correct this problem.

File locations

In order to use our package, we have found it necessary to specify its directory explicitly, but directory names can become quite long and cumbersome.  It would be much better to move our package to a location that is readily accessible by any notebook using a Needs statement rather than loading the package from a specified directory and file name.

Mathematica searches for files using a path variable.  Evaluate $Path to view the path defined for your system.  Somewhere in the middle you will probably find a simple disk name amidst the various folders associated with the operating system and the Mathematica program.  If so, the following procedure should make your package visible to Needs without explicit directory information.

Create a new folder with a name like MyPackages, on the appropriate disk.

Move Fractals.m to this location.

Change Begin["`Fractals`"] to Begin["MyPackages`Fractals`"].

Restart the kernel.  Evaluate Needs["MyPackages`Fractals`"] and inspect the context path — you should find MyPackages`Fractals` present.  Verify that the functions in Fractals.m are available and working.

If your $Path does not provide a suitable location, you have two choices: either move your package to a subdirectory on the path or augment $Path.  For the former we recommend creating a subdirectory of AddOns\ExtraPackages.  For the latter, one must either use the Option Inspector or edit the file that specifies the path, but both methods are system dependent.

Additional features of context commands

Several additional features of the context-manipulation commands are worth discussing briefly.

BeginPackage["context",{need1,need2,⋯}] can take an optional second argument which specifies a list of required packages.  If used in this manner, each of these packages will be entered on the context path as well as the new package being defined.  Sometimes this is a desirable side effect of your package definition, sometimes not.  If you prefer not having the subsidiary packages used in the private implementation become part of the context path returned when your package is loaded, then use a Needs or Get statement following BeginPackage, as we did above.

In order to avoid any confusion between contexts that might occur when errors cause premature termination of package evaluation, it is often useful to omit or comment out the context-manipulation commands during the development phase.  Similarly, it is also useful to inactivate the Protect statement during the development phase.

Begin["NewContext`"] sets the current context to NewContext, but does not add the new context to the context path.

$Context

ClearAll["Global`*"];
Begin["Junk`"];
$Context

?x

x = "stuff";
?x

Begin["`Subcontext`"] appends Subcontext to the current context.

Begin["`MoreJunk`"]

?x

y = "other stuff"

End[] returns $Context to its previous value (before the most recent Begin).

End[];  $Context

?y

?x

Junk`MoreJunk`y

End[];  $Context

x = "global stuff"

Junk`x