24. Object-Oriented Programming

Last revision:  April 15 1998

We continue the study of contexts and packages in the context of object-oriented programming, the theme of this section.

Mathematica operations/structures introduced: 
     Object-Oriented Programming

Basic Object-Oriented Programming

Object-oriented programming refers to a particular style of programming in which programs are written in a way which permits simple, planned expansions and additions by later programmers.  The governing principle is that some functions (the "public" functions) defined by one programmer are meant to be used by further programmers, while other functions and data (the "private" ones) are not meant to be used by someone else, since the functions do very specific tasks with little general value.  This style of programming also encourages programmers to bind together functions and data into a structure called an object; this structure encapsulates all "public" and "private" data and functions and controls access to any of the functions or data by external agents.  (Object-oriented programming languages, such as C++, also provide ways for programmers to use functions without knowing their exact names, using a programming technique called "polymorphism", but this technique is beyond the scope of this course.)  While Mathematica is not an object-oriented programming language per se, we can mimic part of this style of programming by using contexts and packages.  We will not form true objects in the sense of object-oriented programming---you should take a course in object-oriented programming to do so---but we will consider the separation of functions and data into the public and the private.

The method we will use is to decide, first, what data and functions should be "public", common property of any user or programmer using our code. All other data and functions will be deemed "private", and we will enforce this distinction by placing private data and functions in a special context.  We will have two contexts, then, one overarching context which will contain all of our data and functions associated to a particular programming problem, and one context within the first, a private context, which contains those functions and data which we want to prevent others from modifying.

Example:  A Team Roster

As an example, we'll return to our team roster problem.  Recall the following solution to the problem of keeping a list of names, together with the total number of individuals on the team.

Clear[sportsNames,totalNames,addName,deleteName]
sportsNames = {};
totalNames = 0;
addName[s_] := 
			Module[{},
	     (* uses sportsNames, totalNames *)
	     sportsNames = Union[sportsNames,{s}];
	     totalNames = Length[sportsNames];
	  ]
deleteName[s_] :=
	  Module[{},
	  	  (* uses sportsNames, totalNames *)
	  	  sportsNames = Complement[sportsNames,{s}];
	  	  totalNames = Length[sportsNames];
	  ]

addName["Magic"]
addName["Shaq"]
totalNames

How could we improve on this solution?  First, we notice that while we would want others to use the functions "addName[]" and "deleteName[]", we would not  want others to have the ability to change "sportsNames" or "totalNames" independently, if only because they might add a name and forget to increment "totalNames".  Hence we'll need to make "sportsNames" and "totalNames" private data, while "addName" and "deleteName" can be public functions.  Similarly, since we don't want others to initialize our data themselves, we'll provide a public function "sportsInit[]" which they can execute, in which we'll perform the initialization.  Finally, we'll provide a way for a user to request the number of names on the roster, using a public function "getNumber[]", and a way for a user to request the roster list, using the public function "getPlayerList[]".

Before we get started, let's remove all previous definitions so that we don't leave any names in the context "Global`".

Remove[addName,deleteName,sportsNames,totalNames]

Below we'll give a package solution, followed by an analysis of the details of its construction.

BeginPackage["Sports`"];

sportsInit::usage = "sportsInit[] clears the current team roster";

addName::usage = "addName[string] adds a name to the team roster";

deleteName::usage = "deleteName[string] deletes a name from the team roster";

getNumber::usage = "getNumber[] returns the number of players currently on the team roster";

getPlayerList::usage = "getPlayerList[] returns a list of the players on the team roster";

Begin["Sports`Private`"];

sportsInit[] := (sportsNames = {}; totalNames = 0;);
addName[s_String] := 
			Module[{},
	     (* uses sportsNames, totalNames *)
	     sportsNames = Union[sportsNames,{s}];
	     totalNames = Length[sportsNames];
	  ];
deleteName[s_String] :=
	  Module[{},
	  	  (* uses sportsNames, totalNames *)
	  	  sportsNames = Complement[sportsNames,{s}];
	  	  totalNames = Length[sportsNames];
	  ];
getNumber[] := totalNames;
getPlayerList[] := sportsNames;

sportsInit[];

End[];

EndPackage[];

Let's walk through this solution.  First, using "BeginPackage[]", we set up a separate context for our functions and data, so that we won't damage any other functions or data with the same names.  At the end, "EndPackage[]" will add "Sports`" to the context search path, so we'll have access to any functions and data in the context "Sports`".  We then provide usage lines for each public function, a very important step.  These usage lines important because they mention each name which we'll want to be public, thereby establishing those names as names inside the context "Sports`".  Next, we enter a new context, "Sports`Private`".  At this stage any new names declared will be in this separate context, while any names already declared will still be public, in "Sports`".  The effect is that "sportsNames" is a private name, with the long form "Sports`Private`sportsNames", while "getNumber[]" is a public name, with the long form "Sports`getNumber[]".  We make the requisite definitions, and then execute "sportsInit[]", so the initialization will happen whether or not the user remembers explicitly to do so.  Then "End[]" takes us out of the context "Sports`Private`", without adding that context to the context search path, and, finally, "EndPackage[]" ends the context "Sports`", adding "Sports`" to the context search path.

Let's examine how our package works.

getNumber[]

addName["Shaq"]

getPlayerList[]

getNumber[]

Now suppose we want to alter the private data.  We can't seem to get to it!

totalNames

sportsNames

Why is this?  Because the names are part of "Sports`Private`", which is not in the context search path.  It is true that by specifying "Sports`Private`totalNames", we can get to the actual definition, and therefore change the value, but this causes an appropriate burden on the new programmer.  If the package is to be circumvented in its intent, there are still ways to alter the data directly, but the new programmer will have to go to relatively great lengths to do so.  (Incidentally, Mathematica provides certain attributes, such as "Protected", "ReadProtected", and "Locked", which prevent changes to definition, but we won't use them, under the premise that security levels need not be high in our environment!)

Designing Programs Using Packages

Object-oriented concepts, implemented using packages, are therefore desirable components of any Mathematica code.  These object-oriented concepts dovetail nicely with bottom-up analysis, so in designing a package, it is best to attempt both.  Decide what data, and what functions performing simple operations on this data, will be needed before you treat the larger problem.  While you do so, decide which functions and data should be private and which should be public.

The standard format of code for packages is as follows.  First, enter a "BeginPackage[]" statement.  Then, mention each and every name which you will want to be public in your package.  One way to do so is to provide usage lines for each and every item of data and function which the users will use; another way is just to enter the name followed by a semicolon.  Next, use a "Begin[]" statement with a special context name, usually including the name "Private`".  Proceed to enter the definition of each function, public or not, keeping in mind that all new names will be part of the private context.  Finally, exit the private context using "End[]", and finish the package with "EndPackage[]".  To begin, mimic the example above.

IMPORTANT: Realize that in programming a package, the order of execution---and the separation of some lines into their own cells, such as "BeginPackage[]", for example---is crucial.  If you write a package, execute its cells, and then go back to make changes, simply executing the cells you've changed may not perform the operation you desire.  It may be best to execute "Remove["*"]", which removes all user-defined definitions in the "Global`" context, and then "Remove["Context`*"]," substituting for "Context" each of the contexts you are using, separately.  Finally, re-execute each cell, in order.  At this point you will be in a good position to debug your program.  (At worst, you may need to save your file, restart Mathematica, and read the notebook back in.)

Another Example

We wish to design a package which will keep track of many grades of equal weight in a class.  The package should have functions which permit (a) initialization, (b) entering in a grade, (c) removing a grade, (d) printing out current grades, (e) computing the current average, and (f) determining, given the grades already entered, what average grade is necessary on the remaining assignments in order that the final average be a 60, 70, 80, or a 90.  Just as in the example above, we will have private data:  the list of grades.  We will also have public functions, namely, "gradeInit[]", "addGrade[]", "deleteGrade[]", "listGrades[]", "findAverage[]", and "whatDoINeed[]".

BeginPackage["Grades`"];

gradeInit::usage = "gradeInit[] sets the number of current grades to 0";

addGrade::usage = "addGrade[n] adds a numeric grade n to the list of grades";

deleteGrade::usage = "deleteGrade[n] deletes a numeric grade n from the list";

listGrades::usage = "listGrades[] lists the grades currently entered";

findAverage::usage = "findAverage[] determines the average of the current grades";

whatDoINeed::usage = "whatDoINeed[n] determines, given n, the total number of grades in the course, the grades necessary to end the course with an average of 60, 70, 80, and 90";

Begin["Grades`Private`"];

gradeInit[] := (grades={};);

addGrade[n_] := (grades = Append[grades,n];);

deleteGrade[n_] := Module[{a},
			a = Position[grades,n];
			If[Length[a]>=1,
			   grades = Delete[grades,Take[a,1]],
			   Print["Grade not found\n"]];
			];
			
listGrades[] := grades;

findAverage[] := If[Length[grades]>0,
		 Apply[Plus,grades]/Length[grades],
		 Print["No grades\n"]];
		
whatDoINeed[n_] := Module[{a,b,c},
			Which[
			   Length[grades]<1,
			      {60,70,80,90},
			   Length[grades]<n,
	        b[c_] := (c*n-Length[grades]*findAverage[])/
	           (n-Length[grades]);
	        Map[b,{60.0,70.0,80.0,90.0}],
	     Length[grades]>=n,
	     Print["Final average already determined: ",
	        findAverage[]];
	   ]];
	  
gradeInit[];

End[];

EndPackage[];

Here are some sample user commands:

addGrade[90]; addGrade[90]; addGrade[60];
whatDoINeed[4]

The student cannot attain a 90 average, given one more grade, but could attain an 80 average by earning an 80 on the next test.