Copyright (C) 1997 Rich Neidinger, John Swallow, and Todd Will.  Free for
distribution to college and university instructors for personal,
non-commercial use.  If these notebooks are used in a course, the authors
request $20 per student.

Chapter V. Conditional and Recursive Programming

14. Conditional Structures

Last revision:  September 13 1996

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.

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[{abs_,ave_}]:=
If[abs>3,F,
   Which[
      ave>=90,A,
      ave>=80,B,
      ave>=70,C,
      ave>=60,D,
      True,F
   ]
]