12. Structured Patterns

Last revision:  February 18 1998

While some programming languages allow functions which have parameters of complex types, few do so with as much generality as Mathematica.  In this section we consider some of the extensive ways in which Mathematica allows arguments to be recognized in patterns, as well as some examples of how useful such pattern recognition can be.  We also consider optional and default parameters.

Mathematica operations/structures introduced: 
     MatchQ[]
     parameter___
     Count[]
     RotateLeft[]
     :defaultValue

Advanced Patterns

Structured Patterns with Conditions

In the previous section we considered data type checking, boolean tests, conditions, and simple structured patterns, each of which is incorporated in the following example.

Clear[below]
below[{x_Integer,y_?NumberQ} /; x>y]  :=
   "The point is below y=x."

below[{345234,23}]

below[{1,12}]

With regard to conditions, it is important to realize that conditions can be applied only to a single preceding parameter, as in the example above, where the single parameter was a list with two entries.  Conditions cannot be successfully reference the values of any parameters before or after the immediately preceding parameter.  Thus, the following function does not perform correctly.

Clear[below]
below[x_Integer,y_Integer /; x>y] :=
			"The point is below y=x."

below[{345234,23}]

below[{1,12}]

In this section we consider a new sort of pattern matching capability:  allowing a parameter to represent several values, and allowing a parameter to represent several or no values, depending on the input to the function.  We then consider examples of advanced patterns.

Multipart Arguments (Two, Three Underscores, MatchQ)

Recall that we defined a function "switchTwo[]", which switched the first two elements of a three-element list.  What if we wanted to define "switchTwo[]" similarly for a list of any number of elements greater than two? Here is a cumbersome solution:

Clear[switchTwo];
switchTwo[list_List /; Length[list]>1 ] :=
			Join[{list[[2]],list[[1]]},Drop[list,{1,2}]]

switchTwo[{1,2,3,4,5,6}]

Why cumbersome? It is so because the argument is followed by both a data type check and a condition, and the overall definition is much too long for such a simple operation.  Another solution might use a Module and copy the list to another one, altering specific elements in the new list:

Clear[switchTwo];
switchTwo[list_List /; Length[list]>1 ] :=
	Module[{newlist},
		  newlist = list;
		  newlist[[2]]=list[[1]];
		  newlist[[1]]=list[[2]];
		  newlist
	];

switchTwo[{1,2,3,4,5,6}]

Still, this is a long solution.  Instead, we use an appropriate pattern.  If two underscores are placed after a parameter name in a function definition, then Mathematica will allow more than one value to take its place in the argument passed to the function.  For instance, if we define the function as follows, the "rest of the list" beyond the first two elements will be left intact.

Clear[switchTwo];
switchTwo[{a_, b_, c__}] := {b, a, c}

switchTwo[{1,2,3,4,5,6}]

Notice that the parameter "a" was assigned the integer 1, the parameter "b" the integer 2, and the parameter "c" the sequence "3, 4, 5, 6".  The catch here is that "c" is required to have some value; with our current condition, we cannot call "switchTwo[]" with only two parameters, because then "c" would be left without a value.

switchTwo[{1,2}]

If we wish to permit the assignment of no values to the parameter "c", we append to "c"three underscores; this syntax indicates to Mathematica that the argument "c" can take no value, if necessary.

Clear[switchTwo];
switchTwo[{a_, b_, c___}] := {b, a, c}

switchTwo[{1,2,3,4,5,6}]

switchTwo[{1,2}]

switchTwo[{1}]

MatchQ

It is not always obvious whether or not a certain actual parameter matches a pattern required for a function.  Using "MatchQ[]" helps us solve this dilemma:  it performs the pattern-matching for the first argument, given the pattern in the second, and returns a boolean value indicating whether or not the match was successful.

MatchQ[1,t__]

MatchQ[{1,2},{a_,b__,c___}]

MatchQ[{1,2},{a_,b__,c__}]

Using Patterns

An Average Function

Our goal here is to define a general-purpose average function.  We might begin by first defining a function that finds the average of a list.

Clear[ave];
ave[x_] := N[ Apply[ Plus, x ] / Length[x] ]

We must be prepared, however, for some bad input to give us problems.  For instance:

ave["Hello"]

ave[1,2,3]

ave[{1,2,3}]

We therefore improve the function with our usual additions:

Clear[ave];

ave::usage = "ave[list] finds a numeric approximation to the average of the elements of the list";

ave[x_List] := N[ Apply[ Plus, x ] / Length[x] ];

ave[x_] := 
   Print["Your argument \"",x,"\" is not a list."];

ave["Hello"]

ave[1,2,3]

ave[{1,2,3}]

Looking at the action of our function, we notice that we might want "ave[]" to average a sequence of numbers, as in the second case above, even if the numbers are not initially in the form of a list.  We can define "ave[]" to take a list of more than one argument and enclose it in a list, as follows:

Clear[ave];

ave::usage = "ave[list] finds an real number approximation to the average of the elements of the list";

ave[x_List] := N[ Apply[ Plus, x ] / Length[x] ];

ave[x___] := ave[{x}];

ave[x_] := 
   Print["Your argument \"",x,"\" is not a list."]

ave["Hello"]

ave[1,2,3]

ave[{1,2,3}]

Now we have a function which can average several arguments.  What happened with "Hello", though?   It matched "x___" and not "x_List", so "ave["Hello"]" became "ave[{"Hello"}]", and then it was averaged!  Note that the definition "ave[x_] := ..." never executes, since all arguments will be encapsulated in a list or be a list already.  Here is a better solution:

Clear[ave];

ave::usage = "ave[list] finds an real number approximation to the average of the elements of the list";

ave[x_List] := N[ Apply[ Plus, x ] / Length[x] ];

ave[x___?NumberQ] := ave[{x}];

ave[x_] := Print["Your argument \"",x,"\" is not a list."];

ave["Hello"]

ave[1,2,3]

ave[{1,2,3}]

Further improvements are certainly possible:  we have not yet prevented the user from explicitly giving "ave[]" a list of non-numeric values.  Consider how we might improve the function to enforce that constraint.

Counting Random Numbers (Count)

In this example we make use of the Mathematica function "Count[]", which determines how many elements of a list satisfy a certain condition.

?Count

The pattern in the second parameter need not include underscores, data types, or boolean tests; we are permitted to ask for exact matches, by setting the second parameter to the expression we wish matched exactly.  This behavior is analogous to defining a function with parameters without underscores, such as "f[x]=3":  in this case, only when the parameter is exactly "x" will the definition be used.  We might want to know how many zeros appear in a given list.

Count[{1,2,3,0,2,0,4},0]

We might instead ask how many numeric approximations occur in a given list.

Count[{1.2, 1, 3.7, dog }, _Real ]

Note here that "_Real" is a legitimate pattern; we have not included a parameter name, for we do not have a need to reference the value being tested.  In the following example, in which we use a data type check and a condition to test both whether the value is a numeric approximation and whether it is less than 3, we need to refer to the value, so we give it a name:

Count[{1.2, 1, 3.7, dog }, x_Real /; x<3 ]

Using "Count[]", we can now simulate coin tosses, asking how many of 100 random numbers, chosen from 0 to 1, are less than 0.5.

Count[ Table[ Random[],{100}], x_ /; x<.5]

Default Arguments

A default argument is a parameter which, if not specified, is assigned some particular default value.  In the function "N[]", for instance, the second parameter is optional, and if it is not specified, the number of digits for the approximation is determined internally by Mathematica.  Another example is provided by the "RotateLeft[]" function.  This function rotates the list given in the first argument left by the number of positions given in the second argument.

?RotateLeft

But note that if we omit the second argument, the list will be rotated by one position.  The second argument is thus assigned the "default value" 1; if that argument is left out, then "1" is the value it will take.

In order to specify a default parameter inside a function definition, we place a colon (":") followed by the default value after the parameter specification, before the next comma, if applicable.  As an example, suppose we want a function "addOnes[]" which prepends and appends as many 1s as are indicated by the second argument to the list indicated by the first argument.  We might define "addOnes[]" as follows:

Clear[addOnes];
addOnes[l_List,i_Integer:1]:=
	Nest[(Prepend[Append[#,1],1])&,l,i];

addOnes[{2,2},5]

addOnes[{2,2}]

It is true that there is an alternate method of specifying the same function "addOnes[]"; we could write that if there are two arguments, it should have the definition above, while if it has only one argument, it should have an alternate definition, using 1 instead of "i":

addOnes[l_List,i_Integer]:=
	Nest[(Prepend[Append[#,1],1])&,l,i];
addOnes[l_List]:=
	Nest[(Prepend[Append[#,1],1])&,l,1];

Notice that this method eliminates the default argument.  This method is less attractive, however, because, first, a reader must look closely at each function to notice that the functions are basically the same, and, second, Mathematica is required to store two complicated definitions instead of one, wasting memory.

Finally, we should say that we have not covered all of Mathematica's great generality in specifying patterns.  Consult the Help Browser, or see Stephen Wolfram's Mathematica, section 2.3, for more information.