19.1. Boolean Expressions

 < Free Open Study > 

Except for the simplest control structure, the one that calls for the execution of statements in sequence, all control structures depend on the evaluation of boolean expressions.

Using true and false for Boolean Tests

Use the identifiers true and false in boolean expressions rather than using values like 0 and 1. Most modern languages have a boolean data type and provide predefined identifiers for true and false. They make it easy they don't even allow you to assign values other than true or false to boolean variables. Languages that don't have a boolean data type require you to have more discipline to make boolean expressions readable. Here's an example of the problem:

Visual Basic Examples of Using Ambiguous Flags for Boolean Values

Dim printerError As Integer Dim reportSelected As Integer Dim summarySelected As Integer ... If printerError = 0 Then InitializePrinter() If printerError = 1 Then NotifyUserOfError() If reportSelected = 1 Then PrintReport() If summarySelected = 1 Then PrintSummary() If printerError = 0 Then CleanupPrinter()


If using flags like 0 and 1 is common practice, what's wrong with it? It's not clear from reading the code whether the function calls are executed when the tests are true or when they're false. Nothing in the code fragment itself tells you whether 1 represents true and 0 false or whether the opposite is true. It's not even clear that the values 1 and 0 are being used to represent true and false. For example, in the If reportSelected = 1 line, the 1 could easily represent the first report, a 2 the second, a 3 the third; nothing in the code tells you that 1 represents either true or false. It's also easy to write 0 when you mean 1 and vice versa.

Use terms named true and false for tests with boolean expressions. If your language doesn't support such terms directly, create them using preprocessor macros or global variables. The previous code example is rewritten here using Microsoft Visual Basic's built-in True and False:

Good, but Not Great Visual Basic Examples of Using True and False for Tests Instead of Numeric Values
Dim printerError As Boolean Dim reportSelected As ReportType Dim summarySelected As Boolean ... If ( printerError = False ) Then InitializePrinter() If ( printerError = True ) Then NotifyUserOfError() If ( reportSelected = ReportType_First ) Then PrintReport() If ( summarySelected = True ) Then PrintSummary() If ( printerError = False ) Then CleanupPrinter()

Cross-Reference

For an even better approach to making these same tests, see the next code example.


Use of the True and False constants makes the intent clearer. You don't have to remember what 1 and 0 represent, and you won't accidentally reverse them. Moreover, in the rewritten code, it's now clear that some of the 1s and 0s in the original Visual Basic example weren't being used as boolean flags. The If reportSelected = 1 line was not a boolean test at all; it tested whether the first report had been selected.

This approach tells the reader that you're making a boolean test. It's also harder to write true when you mean false than it is to write 1 when you mean 0, and you avoid spreading the magic numbers 0 and 1 throughout your code. Here are some tips on defining true and false in boolean tests:

Compare boolean values to true and false implicitly You can write clearer tests by treating the expressions as boolean expressions. For example, write

while ( not done ) ... while ( a > b ) ...

rather than

while ( done = false ) ... while ( (a > b) = true ) ...

Using implicit comparisons reduces the number of terms that someone reading your code has to keep in mind, and the resulting expressions read more like conversational English. The previous example could be rewritten with even better style like this:

Better Visual Basic Examples of Testing for True and False Implicitly
Dim printerError As Boolean Dim reportSelected As ReportType Dim summarySelected As Boolean ... If ( Not printerError ) Then InitializePrinter() If ( printerError ) Then NotifyUserOfError() If ( reportSelected = ReportType_First ) Then PrintReport() If ( summarySelected ) Then PrintSummary() If ( Not printerError ) Then CleanupPrinter()

If your language doesn't support boolean variables and you have to emulate them, you might not be able to use this technique because emulations of true and false can't always be tested with statements like while ( not done ).

Cross-Reference

For details, see Section 12.5, "Boolean Variables."


Making Complicated Expressions Simple

You can take several steps to simplify complicated expressions:

Break complicated tests into partial tests with new boolean variables Rather than creating a monstrous test with half a dozen terms, assign intermediate values to terms that allow you to perform a simpler test.

Move complicated expressions into boolean functions If a test is repeated often or distracts from the main flow of the program, move the code for the test into a function and test the value of the function. For example, here's a complicated test:

Visual Basic Example of a Complicated Test
If ( ( document.AtEndOfStream ) And ( Not inputError ) ) And _    ( ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES ) ) And _    ( Not ErrorProcessing( ) ) Then    ' do something or other    ... End If

This is an ugly test to have to read through if you're not interested in the test itself. By putting it into a boolean function, you can isolate the test and allow the reader to forget about it unless it's important. Here's how you could put the if test into a function:

Visual Basic Example of a Complicated Test Moved into a Boolean Function, with New Intermediate Variables to Make the Test Clearer
 Function DocumentIsValid( _    ByRef documentToCheck As Document, _    lineCount As Integer, _    inputError As Boolean _    ) As Boolean    Dim allDataRead As Boolean    Dim legalLineCount As Boolean    allDataRead = ( documentToCheck.AtEndOfStream ) And ( Not inputError )       <-- 1    legalLineCount = ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES )       <-- 1    DocumentIsValid = allDataRead And legalLineCount And ( Not ErrorProcessing() ) End Function 

(1)Intermediate variables are introduced here to clarify the test on the final line, below.

Cross-Reference

For details on the technique of using intermediate variables to clarify a boolean test, see "Use boolean variables to document your program" in Section 12.5.


This example assumes that ErrorProcessing() is a boolean function that indicates the current processing status. Now, when you read through the main flow of the code, you don't have to read the complicated test:

Visual Basic Example of the Main Flow of the Code Without the Complicated Test
If ( DocumentIsValid( document, lineCount, inputError ) ) Then    ' do something or other    ... End If

If you use the test only once, you might not think it's worthwhile to put it into a routine. But putting the test into a well-named function improves readability and makes it easier for you to see what your code is doing, and that's a sufficient reason to do it.


The new function name introduces an abstraction into the program that documents the purpose of the test in code. That's even better than documenting the test with comments because the code is more likely to be read than the comments and it's more likely to be kept up to date, too.

Use decision tables to replace complicated conditions Sometimes you have a complicated test involving several variables. It can be helpful to use a decision table to perform the test rather than using ifs or cases. A decision-table lookup is easier to code initially, having only a couple of lines of code and no tricky control structures. This minimization of complexity minimizes the opportunity for mistakes. If your data changes, you can change a decision table without changing the code; you only need to update the contents of the data structure.

Cross-Reference

For details on using tables as substitutes for complicated logic, see Chapter 18, "Table-Driven Methods."


Forming Boolean Expressions Positively

I ain't not no undummy.

Homer Simpson

Not a few people don't have not any trouble understanding a nonshort string of nonpositives that is, most people have trouble understanding a lot of negatives. You can do several things to avoid complicated negative boolean expressions in your programs:

Java Example of a Confusing Negative Boolean Test
 if ( !statusOK ) {       <-- 1    // do something    ... } else {    // do something else    ... } 

(1)Here' the negative not.

You can change this to the following positively expressed test:

Java Example of a Clearer Positive Boolean Test
 if ( statusOK ) {       <-- 1    // do something else    ...       <-- 2 } else {    // do something       <-- 3    ... } 

(1)

(2)Nominal case.

(3)Nominal case.

The second code fragment is logically the same as the first but is easier to read because the negative expression has been changed to a positive.

Cross-Reference

The recommendation to frame boolean expressions positively sometimes contradicts the recommendation to code the nominal case after the if rather than the else. (See Section 15.1, "if Statements.") In such a case, you have to think about the benefits of each approach and decide which is better for your situation.


Alternatively, you could choose a different variable name, one that would reverse the truth value of the test. In the example, you could replace statusOK with ErrorDetected, which would be true when statusOK was false.

Apply DeMorgan's Theorems to simplify boolean tests with negatives DeMorgan's Theorems let you exploit the logical relationship between an expression and a version of the expression that means the same thing because it's doubly negated. For example, you might have a code fragment that contains the following test:

Java Example of a Negative Test
if ( !displayOK || !printerOK ) ...

This is logically equivalent to the following:

Java Example After Applying DeMorgan's Theorems
if ( !( displayOK && printerOK ) ) ...

Here you don't have to flip-flop if and else clauses; the expressions in the last two code fragments are logically equivalent. To apply DeMorgan's Theorems to the logical operator and or the logical operator or and a pair of operands, you negate each of the operands, switch the ands and ors, and negate the entire expression. Table 19-1 summarizes the possible transformations under DeMorgan's Theorems.

Table 19-1. Transformations of Logical Expressions Under DeMorgan's Theorems

Initial Expression

Equivalent Expression

not A and not B

not ( A or B )

not A and B

not ( A or not B )

A and not B

not ( not A or B )

A and B

not ( not A or not B )

not A or not B[*]

not ( A and B )

not A or B

not ( A and not B )

A or not B

not ( not A and B )

A or B

not ( not A and not B )


[*] This is the expression used in the example.

Using Parentheses to Clarify Boolean Expressions

If you have a complicated boolean expression, rather than relying on the language's evaluation order, parenthesize to make your meaning clear. Using parentheses makes less of a demand on your reader, who might not understand the subtleties of how your language evaluates boolean expressions. If you're smart, you won't depend on your own or your reader's in-depth memorization of evaluation precedence especially when you have to switch among two or more languages. Using parentheses isn't like sending a telegram: you're not charged for each character the extra characters are free.

Cross-Reference

For an example of using parentheses to clarify other kinds of expressions, see "Parentheses" in Section 31.2.


Here's an expression with too few parentheses:

Java Example of an Expression Containing Too Few Parentheses
if ( a < b == c == d ) ...

This is a confusing expression to begin with, and it's even more confusing because it's not clear whether the coder means to test (a < b ) == ( c == d ) or ( ( a < b ) == c ) == d. The following version of the expression is still a little confusing, but the parentheses help:

Java Example of an Expression Better Parenthesized
if ( ( a < b ) == ( c == d ) ) ...

In this case, the parentheses help readability and the program's correctness the compiler wouldn't have interpreted the first code fragment this way. When in doubt, parenthesize.

Use a simple counting technique to balance parentheses If you have trouble telling whether parentheses balance, here's a simple counting trick that helps. Start by saying "zero." Move along the expression, left to right. When you encounter an opening parenthesis, say "one." Each time you encounter another opening parenthesis, increase the number you say. Each time you encounter a closing parenthesis, decrease the number you say. If, at the end of the expression, you're back to 0, your parentheses are balanced.

Cross-Reference

Many programmer-oriented text editors have commands that match parentheses, brackets, and braces. For details on programming editors, see "Editing" in Section 30.2.


Java Example of Balanced Parentheses
 if ( ( ( a < b ) == ( c == d ) ) && !done ) ...       <-- 1    | | |          |    |        | |                  | 0  1 2 3          2    3        2 1                  0       <-- 2 

(1)Read this.

(2)Say this.

In this example, you ended with a 0, so the parentheses are balanced. In the next example, the parentheses aren't balanced:

Java Example of Unbalanced Parentheses
 if ( ( a < b ) == ( c == d ) ) && !done ) ...       <-- 1    | |               |        | |                  | 0  1 2 3          2    3        2 1                  0       <-- 2 

(1)Read this.

(2)Say this.

The 0 before you get to the last closing parenthesis is a tip-off that a parenthesis is missing before that point. You shouldn't get a 0 until the last parenthesis of the expression.

Fully parenthesize logical expressions Parentheses are cheap, and they aid readability. Fully parenthesizing logical expressions as a matter of habit is good practice.

Knowing How Boolean Expressions Are Evaluated

Many languages have an implied form of control that comes into play in the evaluation of boolean expressions. Compilers for some languages evaluate each term in a boolean expression before combining the terms and evaluating the whole expression. Compilers for other languages have "short-circuit" or "lazy" evaluation, evaluating only the pieces necessary. This is particularly significant when, depending on the results of the first test, you might not want the second test to be executed. For example, suppose you're checking the elements of an array and you have the following test:

Pseudocode Example of an Erroneous Test
while ( i < MAX_ELEMENTS and item[ i ] <> 0 ) ...

If this whole expression is evaluated, you'll get an error on the last pass through the loop. The variable i equals maxElements, so the expression item[ i ] is equivalent to item[ maxElements ], which is an array-index error. You might argue that it doesn't matter since you're only looking at the value, not changing it. But it's sloppy programming practice and could confuse someone reading the code. In many environments it will also generate either a run-time error or a protection violation.

In pseudocode, you could restructure the test so that the error doesn't occur:

Pseudocode Example of a Correctly Restructured Test
while ( i < MAX_ELEMENTS )    if ( item[ i ] <> 0 ) then       ...

This is correct because item[ i ] isn't evaluated unless i is less than maxElements.

Many modern languages provide facilities that prevent this kind of error from happening in the first place. For example, C++ uses short-circuit evaluation: if the first operand of the and is false, the second isn't evaluated because the whole expression would be false anyway. In other words, in C++ the only part of

if ( SomethingFalse && SomeCondition ) ...

that's evaluated is SomethingFalse. Evaluation stops as soon as SomethingFalse is identified as false.

Evaluation is similarly short-circuited with the or operator. In C++ and Java, the only part of

if ( somethingTrue || someCondition ) ...

that is evaluated is somethingTrue. The evaluation stops as soon as somethingTrue is identified as true because the expression is always true if any part of it is true. As a result of this method of evaluation, the following statement is a fine, legal statement.

Java Example of a Test That Works Because of Short-Circuit Evaluation
if ( ( denominator != 0 ) && ( ( item / denominator ) > MIN_VALUE ) ) ...

If this full expression were evaluated when denominator equaled 0, the division in the second operand would produce a divide-by-zero error. But since the second part isn't evaluated unless the first part is true, it is never evaluated when denominator equals 0, so no divide-by-zero error occurs.

On the other hand, because the && (and) is evaluated left to right, the following logically equivalent statement doesn't work:

Java Example of a Test That Short-Circuit Evaluation Doesn't Rescue
if ( ( ( item / denominator ) > MIN_VALUE ) && ( denominator != 0 ) ) ...

In this case, item / denominator is evaluated before denominator != 0. Consequently, this code commits the divide-by-zero error.

Java further complicates this picture by providing "logical" operators. Java's logical & and | operators guarantee that all terms will be fully evaluated regardless of whether the truth or falsity of the expression could be determined without a full evaluation. In other words, in Java, this is safe:

Java Example of a Test That Works Because of Short-Circuit (Conditional) Evaluation
if ( ( denominator != 0 ) && ( ( item / denominator ) > MIN_VALUE ) ) ...

But this is not safe:

Java Example of a Test That Doesn't Work Because Short-Circuit Evaluation Isn't Guaranteed
if ( ( denominator != 0 ) & ( ( item / denominator ) > MIN_VALUE ) ) ...

Different languages use different kinds of evaluation, and language implementers tend to take liberties with expression evaluation, so check the manual for the specific version of the language you're using to find out what kind of evaluation your language uses. Better yet, since a reader of your code might not be as sharp as you are, use nested tests to clarify your intentions instead of depending on evaluation order and short-circuit evaluation.


Writing Numeric Expressions in Number-Line Order

Organize numeric tests so that they follow the points on a number line. In general, structure your numeric tests so that you have comparisons like these:

MIN_ELEMENTS <= i and i <= MAX_ELEMENTS i < MIN_ELEMENTS or MAX_ELEMENTS < i

The idea is to order the elements left to right, from smallest to largest. In the first line, MIN_ELEMENTS and MAX_ELEMENTS are the two endpoints, so they go at the ends. The variable i is supposed to be between them, so it goes in the middle. In the second example, you're testing whether i is outside the range, so i goes on the outside of the test at either end and MIN_ELEMENTS and MAX_ELEMENTS go on the inside. This approach maps easily to a visual image of the comparison in Figure 19-1:

Figure 19-1. Examples of using number-line ordering for boolean tests


If you're testing i against MIN_ELEMENTS only, the position of i varies depending on where i is when the test is successful. If i is supposed to be smaller, you'll have a test like this:

while ( i < MIN_ELEMENTS ) ...

But if i is supposed to be larger, you'll have a test like this:

while ( MIN_ELEMENTS < i ) ...

This approach is clearer than tests like

( i > MIN_ELEMENTS ) and ( i < MAX_ELEMENTS )

which give the reader no help in visualizing what is being tested.

Guidelines for Comparisons to 0

Programming languages use 0 for several purposes. It's a numeric value. It's a null terminator in a string. It's the value of a null pointer. It's the value of the first item in an enumeration. It's false in logical expressions. Because it's used for so many purposes, you should write code that highlights the specific way 0 is used.

Compare logical variables implicitly As mentioned earlier, it's appropriate to write logical expressions such as

while ( !done ) ...

This implicit comparison to 0 is appropriate because the comparison is in a logical expression.

Compare numbers to 0 Although it's appropriate to compare logical expressions implicitly, you should compare numeric expressions explicitly. For numbers, write

while ( balance != 0 ) ...

rather than

while ( balance ) ...

Compare characters to the null terminator ('\0') explicitly in C Characters, like numbers, aren't logical expressions. Thus, for characters, write

while ( *charPtr != '\0' ) ...

rather than

while ( *charPtr ) ...

This recommendation goes against the common C convention for handling character data (as in the second example here), but it reinforces the idea that the expression is working with character data rather than logical data. Some C conventions aren't based on maximizing readability or maintainability, and this is an example of one. Fortunately, this whole issue is fading into the sunset as more code is written using C++ and STL strings.

Compare pointers to NULL For pointers, write

while ( bufferPtr != NULL ) ...

rather than

while ( bufferPtr ) ...

Like the recommendation for characters, this one goes against the established C convention, but the gain in readability justifies it.

Common Problems with Boolean Expressions

Boolean expressions are subject to a few additional pitfalls that pertain to specific languages:

In C-derived languages, put constants on the left side of comparisons C-derived languages pose some special problems with boolean expressions. If you have problems mistyping = instead of ==, consider the programming convention of putting constants and literals on the left sides of expressions, like this:

C++ Example of Putting a Constant on the Left Side of an Expression---An Error the Compiler Will Catch
if ( MIN_ELEMENTS = i ) ...

In this expression, the compiler should flag the single = as an error since assigning anything to a constant is invalid. In contrast, in the following expression, the compiler will flag this only as a warning, and only if you have compiler warnings fully turned on:

C++ Example of Putting a Constant on the Right Side of an Expression---An Error the Compiler Might Not Catch
if ( i = MIN_ELEMENTS ) ...

This recommendation conflicts with the recommendation to use number-line ordering. My personal preference is to use number-line ordering and let the compiler warn me about unintended assignments.

In C++, consider creating preprocessor macro substitutions for &&, ||, and == (but only as a last resort) If you have such a problem, it's possible to create #define macros for boolean and and or, and use AND and OR instead of && and ||. Similarly, using = when you mean == is an easy mistake to make. If you get stung often by this one, you might create a macro like EQUALS for logical equals (==).

Many experienced programmers view this approach as aiding readability for the programmer who can't keep details of the programming language straight but as degrading readability for the programmer who is more fluent in the language. In addition, most compilers will provide error warnings for usages of assignment and bitwise operators that seem like errors. Turning on full compiler warnings is usually a better option than creating nonstandard macros.

In Java, know the difference between a==b and a.equals(b) In Java, a==b tests for whether a and b refer to the same object, whereas a.equals(b) tests for whether the objects have the same logical value. In general, Java programs should use expressions like a.equals(b) rather than a==b.

 < Free Open Study > 


Code Complete
Code Complete: A Practical Handbook of Software Construction, Second Edition
ISBN: 0735619670
EAN: 2147483647
Year: 2003
Pages: 334

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net