@Peter Lippolt here is an explanation from the instructor Dave Withoff
The essential issue raised by the example
In[]:= f[x_ /; x < 1] := 1
In[]:= f[x_ /; x >= 1] := x x x
In[]:= Sqrt[f[x_] /; x >= -2 && x <= -1] ^:= 3
In[]:= Sqrt[f[-1.5]]
Out[]= 1
is the question of what the system should do when two rules apply to
the same input, rather than a specific feature of upvalues.
The problem is that the rule for f[x_ /; x < 1] and the rule for
Sqrt[f[x_] /; x >= -2 && x <= -1] both apply to Sqrt[f[-1.5]]. Without
further information, the computer has no way of knowing which rule to
use.
In the Wolfram Language, the default, following the principle of
standard order evaluation (evaluating the elements of an expression
before evaluating the enclosing expression), is to evaluate f[-1.5],
which has the effect of applying the rule for f[x_ /; x < 1].
There are many ways to get the rules applied in a different order. One
method is to enter all of the rules as upvalues for f:
In[]:= Sqrt[f[x_] /; x >= -2 && x <= -1] ^:= 3
In[]:= f /: _[f[x_ /; x < 1]] := 1
In[]:= f /: _[f[x_ /; x >= 1]] := x x x
In[]:= Sqrt[f[-1.5]]
Out[]= 3
Entering all of the rules as upvalues like this is closer conceptually
to the way this would work in an OOP system. Mixing upvalues and
downvalues potentially increases confusion about which will be used
first.
A few observations about this behavior:
1) Comparable issues will arise in any system, including OOP systems.
If two rules, or two methods, apply in the same situation, the
computer has to somehow resolve the conflict. Different systems will
handle the conflict in different ways, but the conflict will remain.
2) Conflicts like this are rare in practical applications. It is easy
to make up examples where the system is presented with ambiguous
instructions and has to follow some default or some process for
deciding what to do, but in practical applications such ambiguities
usually either don't come up in the first place, or are resolved in
some other way.
3) To address the question about real-life uses of upvalues, there are
a number of large applications of upvalues. Two prominent examples are
the rules for series expansions, and implementation of non-commutative
algebra.
The built-in rules for series expansions are implemented as upvalues
for SeriesData. SeriesData is the symbol that is used for representing
series expansions. For convenience of programming, the rules for
series expansions are implemented as upvalues for SeriesData rather
than as downvalues for individual functions.
For example, Series[Exp[x], {x, 0, 3}] is evaluated by first
constructing SeriesData[x, 0, {1}, 1, 4, 1], which is the series
expansion to degree 3 for x by itself, and then applying Exp to that
SeriesData expression. The result is normally formatted as a sum, but
the SeriesData expression can be seen using InputForm:
In[]:= Exp[SeriesData[x, 0, {1}, 1, 4, 1]] // InputForm
Out[]//InputForm= SeriesData[x, 0, {1, 1, 1/2, 1/6}, 0, 4, 1]
The rules that gave that result, and the rules for doing series
expansions of other functions, are implemented as upvalues for
SeriesData. Comparable rules could be entered as downvalues for
individual functions, such as the Exp function, but it is easier to do
the programming if all of the rules for series expansions are in one
place, attached to SeriesData, rather than scattered throughout the
system among all of the other rules for individual functions.
Similarly, it is convenient when entering rules for non-commutative
algebra to attach rules as upvalues for elements of the algebra. For
example, using the ** notation for NonCommutativeMultiply:
In[]:= x /: x[p_] ** x[q_] := -x[q] ** x[p] /; ! OrderedQ[{p, q}]
In[]:= x[2] ** x[1]
Out[]= -x[1] ** x[2]
A comparable rule could have been entered by unprotecting the built-in
symbol NonCommutativeMultiply and entering a downvalue for
NonCommutativeMultiply, but some consider it easier or more natural to
instead enter a rule like this as upvalue for x.