Chapter V. Conditional and Recursive Programming

15. Conditional Structures

Last revision:  February 18 1998

In this chapter we consider two new styles of programming, namely conditional and recursive programming, which are in some respects more versatile and in some respects less elegant and efficient than list-based programming.  This style of programming permits conditional structures ("If[]", "Which[]", and "Switch[]" statements) which allow different expressions to be evaluated, depending on the result of a test, during the execution of a module, and it also allows other structures for repetition, including recursion and loops (using "Do[]", "While[]", or "For[]").  In this section we begin with structures for conditional expressions.

Mathematica operations/structures introduced: 
     If[]
     Which[]
     Switch[]

Conditional Structures

Most programs perform different operations depending on different sorts of input.  For instance, in a course registration program, the computer will need to perform differently depending on what the user's choice of action is, which may include (a) registering a student for a course, (b) dropping a student from a course, (c) placing the student on a wait-list, or (d) determining the ceiling for a course.  To implement different courses of action, the programmer must first design a test based on the input (in our example, the menu item the user presumably chose) and then have the computer, based on the results of the test, choose an appropriate set of instructions to execute.  For instance, if the user entered "R", the course of action might be to register a student, "D" to drop the student, and so on.  Note that we have already seen one method to write code which performs differently depending on different input, by providing patterns in function definitions, with type checking and/or additional conditional expressions.

This section examines commands for providing conditional structures inside functions instead of inside their argument lists, permitting the programmer to encapsulate different sets of instructions inside the same definition.  For example, consider our factorial function definition:

Clear[fac]
fac[1] := 1
fac[n_Integer?((#>0)&)] := n fac[n-1]

Here we are using two separate definitions for our factorial function.  With an "If[]" statement, which we will define below, we can place both expressions inside the same definition, since the "If[]" statement will implement the test.  Here is how it would look:

Clear[fac]
fac[n_Integer] := If[n>1,n fac[n-1],If[n==1,1]]

We read the function as follows:  "fac[]" is a function which is valid for integers only.  If the argument is greater than 1 (so that the "n>1" test returns a True), then the function will execute "n fac[n-1]"; otherwise the function will execute the next "If" statement.  If it does execute the next "If" statement, it checks whether or not the argument equals 1.  If so, it returns a 1; if not, it does nothing.  We test our function below.

fac[5]

fac[1]

fac[-1]

Functional Form of Conditional Evaluation (If)

We are now in a position to explore the syntax of the "If[]" command, as follows.  The first argument must be a boolean expression.  If the first argument evaluates to a True, the operations in the second argument are executed, while if the first argument evaluates to a False, the operations in the third argument (if it exists) are executed.

For example, suppose we want to define a function which returns a 1 if the argument is greater than 0 and a -1 if the argument is less than or equal to zero.  We can accomplish this with a single "If[]" statement:

Clear[f1]
f1[n_] := If[n>0,1,-1]

Notice that the alternative, using multiple definitions of the function, would be, for instance,

Clear[f1]
f1[n_?((#>0)&)] := 1
f1[n_?((#<=0)&)] := -1

which is not nearly as elegant, or as efficient, since Mathematica must store two full definitions of "f1[]".

Suppose we also want the function to return a 0 if the argument is exactly zero, then using "If[]"s we must place an "If[]" inside another "If[]".  (Invoking one function inside an invocation of the same function is sometimes called nesting the function.)

Clear[f1]
f1[n_] := If[n>0,1,If[n==0,0,-1]]

If the argument is greater than 0, "f1[]" returns a 1; if not, if it is equal to zero, it returns a 0;
otherwise it returns a -1.

Long Sequences of Choices (Which, Switch)

Which

It would seem appropriate to have a special syntactical structure for sequences of "If[]"s, say for instance for the example above, when two tests needed to be made, one if the argument were greater than 0 and the other if the argument were equal to 0.  We can accomplish this with a simple syntactical construct, the "Which[]" statement.  The "Which[]" statement requires an even number of arguments, and its action is to consider the arguments in pairs, beginning with the first pair.  If the first element of a pair is evaluates to True, the second element is executed and no other pairs are considered.  If the first element of a pair is False, then Mathematica moves on to the next pair.

Our 1\0\-1 function above might then be redefined as follows:

Clear[f2]
f2[n_] := Which[n>0,1,n==0,0,n<0,-1]

Similarly, our factorial function could be defined with a "Which[]" statement:

Clear[fac]
fac[n_] := Which[n>1,n fac[n-1],n==1,1]

Switch

We might also want to check whether an expression matches one of several patterns and perform a different operation depending on the matching pattern.  If the expression is an integer, say, we might want to return its real approximation using "N[]"; if it is of the Rational data type, we might want also to return its real approximation; but if it is a list, we might want the approximation of only the first element of the list.  We can do so by using a "Switch[]" construct.  An evaluation of "Switch[]" first evaluates the first argument, and then considers the remainder of the arguments in pairs.  If the first element of a pair is a pattern to which the first argument matches, the second element of the pair is executed and no other pairs are considered.  If the first element of a pair is a pattern to which the first argument does not match, then Mathematica moves on to the next pair.  Here is how we would accomplish the sequence of matches above using "Switch[]".  (Note that without using "Switch[]", we would have to use "MatchQ[]" and "If[]" statements or "Which[]" statements.)

Clear[myfunc]
myfunc[n_] := Switch[
   n,
   _Integer,  N[n],
   _Rational, N[n],
   _List,     N[ n[[1]] ]
   ]

myfunc[1]

myfunc[1/2]

myfunc[{Pi,2,3}]

We note that the syntax for "Switch[]" is very similar to that of "Which[]", except that a first argument is the expression which will be tested against the patterns which are the first elements of the succeeding pairs in the argument list.

Examples Using Conditional Structures

Grades

"If[]" statements can be nested, as we have seen above; it is possible to place an "If[]" statement inside another one.  Here we write a function that turns a number representing a percentage into a letter grade.  If the average is at least a 90, then an A is output; otherwise the next "If[]" statement is used.  In this next "If[]" statement, if the average is at least 80, then a B is output, and so on.

Clear[grade]
grade[ave_]:=
If[ave>=90,A,
   If[ave>=80,B,
      If[ave>=70,C,
         If[ave>=60,D,F]
      ]
   ]
]

A more elegant solution, however, is to use a "Which[]" statement, as follows:

Clear[grade]
grade[ave_]:=
Which[
  ave>=90,A,
  ave>=80,B,
  ave>=70,C,
  ave>=60,D,
  True,F
]

Note: to insure that a "Which[]" statement outputs something, make the first element of the last pair be the value True. Then, if none of the previous tests turn out to be true, then at least the second element of the last pair will be returned. In the case above, if none of the previous tests are true, the student receives an F.

Combining Conditions

Now we take our previous "Which[]" statement, which computes a grade for a class using the standard correspondence of averages and letter grades, and add a rule to the effect that missing four (Tuesday, Thursday) classes results in a F.  If the number of absences and the percent average for a student are given in a list as a parameter, here is how we might define a function to compute the grade.

Clear[grade]
grade[{absences_,ave_}]:=
If[absences>3,F,
   Which[
      ave>=90,A,
      ave>=80,B,
      ave>=70,C,
      ave>=60,D,
      True,F
   ]
]