CS 3411C - April 22, 1999

Lecture 8 -- Expressions and Assignment Statements


Expressions and Assignment Statements
 
This topic sounds boring too, but it's way more interesting than data types.  
Why?  Because in large part, this is what programming languages do--they 
compute, or at least they let you compute.  And that all happens in 
expressions and assignment statements.  Understand this stuff well, with 
all its implications, and you're well on the way to being a great programmer.
 

Expressions
 
As your book says, "The essence of the imperative programming languages is 
the dominant role of assignment statements."  The simple assignment statement 
specifies an expression to be evaluated (the right hand side of the 
assignment statement) and a target location in which to place the result of 
the evaluations (the left hand side).   Assignment statements can be more 
complex than this, however.
 
Getting at the meaning or semantics of an expression involves knowing 
  o operator precedence--the order in which operators are evaluated
  o associativity--the order in which operators are evaluated when
    they all have the same precedence
 

Arithmetic Expressions
 
The arithmetic expressions you see in programming languages inherit lots of 
characteristics from conventional  math usage.  Arithmetic expressions 
consist of
  o operators
  o operands
  o parentheses
  o function calls
 
Operators can work on one operand (unary) or two operands (binary), and some 
languages have operators that can take three operands (C, C++, and Java have 
ternary operators).
 
The notation for expressions in imperative languages tends to be infix 
(operand operator operand).  Sometimes it can be prefix (operator operand 
operand).
 

Operator evaluation order
 
Operator precedence rules define the order in which operators of different 
precedence levels are evaluated.  WhatUs a precedence level?  It comes from 
math.  For example, in math, multiplication takes precedence over addition, 
so in an expression like
 
A + B * C
 
the B * C part is computed first, and then that result is added to A.
 
Most modern programming languages have all sorts of operators that can show 
up in an expression, so language designers have to be careful to specify 
operator precedence rules, often through something called a precedence table.
 
Usually, exponentiation gets the highest precedence, with multiplication and 
division the next highest, followed by addition and subtraction.  If there's 
unary addition or subtraction, then they'll have higher precedence than their 
binary counterparts.


Associativity
 
What happens if you're evaluating an expression, and all the operators have 
the same precedence level?  The answer depends on the associativity rules of 
the language.  If an operator has left associativity, then the leftmost 
occurrence of that operator is evaluated first.  If the operator has right 
associativity, then the rightmost occurrence of that operator is evaluated 
first.  
 
Usually, associativity is left to right (in imperative languages), except 
with exponentiation which is right to left.  In other words,
 
A - B + C
 
Means that you compute A - B first, then add the result to C.  On the 
other hand,
 
A ** B ** C
 
means you raise B to the Cth power, and then you raise A to whatever 
number that is.  Thus 
 
2 ** 3 ** 2
 
    9        2
is 2   not  8
 
If you want to change associativity order, it's easy.  Just do what the 
math folks do...sprinkle in some parentheses.
 
(2 ** 3) ** 2
                    2
really does become 8
 
You can overuse parentheses, however, and make your code hard to write and 
harder to read.
 
Pathologically aberrant languages just confuse the heck out of things.  As we 
mentioned before, APL works right to left, and all operators have the same 
precedence.  So
 
A x B + C
 
Adds B to C then multiplies the result by A.
 
The moral here is to always understand associativity and precedence for any 
language you use.  Assuming one rule when another is in use will lead to bugs 
that are really really hard to find.
 

Conditionals...the ternary operator
 
C, C++, and Java allow you to write a conditional expression like this (in 
this example it's in an assignment statement):
 
average = (count == 0) ? 0 : sum / count ;
 
Which says " if count equals 0, bind average to 0 otherwise bind average to 
sum divided by count"...easy to write, but not so easy to read.  In this 
case, the ?: pair is considered to be a single, ternary operator (with an 
operand sandwiched between the two halves of the operator).
 
 
Operand evaluation order
 
With good programming practice, the order in which operands are evaluated 
within an expression shouldn't have any bearing on the result, since you 
would never write an expression in such a way that evaluating one operand 
would have a side effect that changed another operand, would you?
 
Well of course you would.  And if you had an expression like this
 
B :=  FUN1(A) + FUN2(A)
 
And FUN1 and FUN2 both altered the value of A in some way, then the value of 
B is going to depend on whether you evaluate operands left-to-right or 
right-to-left.
 
How to get around this ugly mess.  One way, the functional programming 
approach, is to not allow side effects.  This makes programming hard for 
functionally-challenged programmers, however, and it can have a negative 
impact on the almighty efficiency.  (What kind?  He didn't say.)
 
Another thing that can be done is to specify a strict evaluation order in the 
language definition, but making sure that language implementors enforce and 
programmers know about it is a whole another problem.
 
FORTRAN 77 goes down yet another road, saying that expressions with function 
calls are valid only if the functions do not change the values of other 
operands in that expression.  It's neat, but it's really hard for a compiler 
to test those conditions.
 

Overloaded operators
 
Operator overloading means that an operator has multiple meanings or uses, 
depending on context.  It's usually not a big deal, and we take for granted 
that it's generally a good thing as long as it doesn't hurt readability or 
reliability.
 
For example, the + operator just seems like addition to us, but to the 
computer itself, adding two integers is a whole lot different than adding two 
floating-point values.  Those are whole different instructions at the machine 
level, but we use the same operator to represent both.  It makes sense to us, 
it's convenient, and it happens to be a really good example of useful 
abstraction.
 
 
Type Conversions
 
We want to be able to write mixed-mode expressions...for example, we really 
want to be able to add integers to floats and get something that is useful, 
not some type compatibility error.  To make this possible, the programming 
language has to convert the type of at least one of the operands to be 
something that's compatible with the other, and to do so as best as possible 
without losing information.
 
In the case of adding integers to floats, we want to convert the integer to a 
float, and not vice-versa.  Why?  If we convert the float to an integer, we're
going to lose everything past the decimal point, and that would be bad.
 
So languages let us convert type when it's possible.  If the programming 
language does it for us implicitly, that's called coercion.  If the language 
has the programmer call for it explicitly, that's called casting.  
 

Relational and Boolean Expressions
 
Vocabulary time:
 
o A relational operator is an operator that compares the values of
  its two operands.
 
o A relational  expression has two operands and one relational 
  operator.  It returns a Boolean value (except when there's no
  boolean type).
 
o Boolean operators take Boolean operands.  
 
o A Boolean expression has Boolean variables, Boolean operators,
  Boolean constants, and relational expressions (that return 
  Boolean values).
 
Boolean operators are not the same as relational operators.
 
Precedence becomes a factor again.  Relational operators are near the bottom, 
and Boolean AND is lower, and then Boolean OR is usually lower still.
 
C is notable among modern languages in that it doesn't have a Boolean type.  
It treats the integer 0 as false and 1 as true.
 

Short-circuit evaluation
 
The short-circuit evaluation of an expression is one in which the result is 
determined without evaluating all of the operands and/or operators.
 
Here's an example from LISP:  (or  expr1 expr2 expr3)
 
If expr1 is true, do you need to evaluate expr2 or expr3?  No.  But what if 
expr2 or expr3 had side effects that you were counting on, and you weren't 
aware of short-circuit evaluation in LISP?  Then you have a bug that, again, 
is going to be hard to find.
 

Assignment
 
Once you understand the evaluation of expressions, the assignment thing is 
easy.  Syntax is generally , and you should know that 
on the left hand side of an assignment statement you can have (depending on 
the language)
 
o  a single target (destination)   A = B
   but you might also have         A = B = C (in PL/1, for example)
o  multiple targets                A, B = C
o  conditional targets       flag ? count1 : count2 = 0;  (in C++ and Java)
 
To make things more complicated, you can have
 
o compound assignment           A += B;  is same as A = A + B;   (in C)
 
and last but not least, on the right hand side, you can have
 
o an assignment statement as an expression, in languages where
  an assignment statement returns a value  (LISP, C, C++, Java):
 
   (setq X (setq Y 5))
 
and now X and Y both are 5.

Last revised: April 25, 1999