As I grow more interested in programming languages—and languages in general—I find that the theory doesn’t always match up with reality. For instance, I just learned about the difference between statements and expressions and how that difference isn’t always explicit in modern programming languages.
Table of Contents
Background
As a current PhD student and Graduate Teaching Assistant, I’ve been focusing a lot on what it takes to be a good professor. To do that, I’ve been learning from different faculty about their experiences and philosophies. Recently, I learned about the difference between statements and expressions, so I thought that would be fun to share with you.
Oddly enough, I actually learned the distinction the hard way while training to teach a software fundamentals course. As a part of that training, I had to complete all the programming assignments, so I could get feedback from the instructor. At one point, the instructor mentioned to me that they didn’t like the following Java syntax:
a[++i]
In this case, we have an array that we’re accessing through ++i
. In other words, we increment i
then access a
at that index—all in one line. See any problems? If not, don’t worry! That’s the topic of today’s article.
Terminology
Right out of the gate, I’d like to differentiate two terms: expression and statement. These terms will form the basis of the argument behind why a[++i]
is considered bad practice.
Expressions
In Computer Science, when we talk about expressions, we’re referring to anything that can be evaluated to produce a value. Naturally, we can think of any data by itself as an expression because data always evaluates to itself:
4 "Hi!" x 'w' true 9.0
Of course, expressions can be made up of expressions:
4 + 2 "Hi," + " friend!" x * y 'w' + 4 true == !false 9.0 / 3
In each of these scenarios, we use operators to nest our expressions, so we get something that might look like the following language grammar:
<expr>: number | (<expr>) | <expr> * <expr> | <expr> + <expr>
Here, we’ve created a silly grammar which defines an expression as a number, an expression in parentheses, an expression times an expression, or an expression plus an expression. As you can probably imagine, there are a lot of ways to write an expression. The only rule is that the expression must return a value.
Statements
In contrast, statements do not return anything. Instead, they perform an action which introduces some form of state (aka a side effect). The following list contains a few examples of statements:
x = 5 if (y) { ... } while (true) { ... } return s
If we look closely, we might notice that some statements contain expressions. However, the statements themselves do not evaluate to anything.
The interesting thing about statements is that they depend on order. To make sense of some statement, it’s important to understand the context leading up to it.
In contrast, expressions don’t depend on state since they do not produce side effects, so any nested expression can be reasoned about directly. For instance, notice how we can isolate any part of the following expression and evaluate its result:
((6 * 7) + (5 + 2 + 1)) > 17
Sure, any outer scope is going to depend on the result of some inner scope, but evaluating (6 * 7)
has no effect on 17
. As a result, it’s very easy to reason about the expression even when elements of it change. Welcome to the foundations of functional programming—but, that’s a topic for a different time!
What’s the Catch?
Unfortunately, while the definitions I’ve provided are clean cut, modern programming languages don’t always adhere to the same principles. For example, is ++i
a statement or an expression? Trick question: it may be both.
In Java, ++i
and i++
can be used as standalone statements to change the state of the program. For instance, they’re often used to increment a variable in a for loop. In addition, however, they can be used as expressions:
a[++i] a[i++] someFunction(i++)
In other words, ++i
returns a value, and that value is different from i++
. As you can probably imagine, this ambiguity between statements and expressions can manifest itself into some nasty bugs. For example, what do you think the following program does?
i = 0 while (i < 5) { print(i) i = i++ }
Without getting into the weeds, this code snippet may do many different things. In Java, it will actually print zero indefinitely despite clearly incrementing i
in the 4th line. As it turns out, the postfix ++
operator returns the old value of i
after increasing its value by one. In other words, i
is incremented then reset to zero.
The consequences of the ambiguity between statements and expressions is immense, and it carries over into functions and procedures as well.
But Wait, There’s More
Often times, terms like methods, functions, procedures, and subroutines are all used interchangeably. In fact, you’ll probably find that I hardly differentiate between the terms on my own site. That said, there is a subtle difference at least between functions and procedures, so let’s talk about it.
Functions
Like mathematical functions, programming functions return a value given some input:
int getLength(String s) { ... } double computeAreaOfSquare(double length) { ... } double computePotentialEnergy(double m, double g, double h) { ... }
In other words, the return type of a function cannot be nothing (i.e. void). As a result, functions are similar to expressions: they return a value without any side effects. In fact, they often work in the place of expressions:
(getLength(s1) * 2) > getLength(s2)
By definition, a function would then be an expression.
Procedures
In contrast, procedures do not return a value. Instead, they perform some action:
void scale(Square sq, double sc) { ... } void insertElementAt(int[] list, int index, int element) { ... } void mutateString(char[] str) { ... }
As a result, procedures relate more closely to statements in that they only produce side effects. Naturally, they cannot be used as expressions:
mutateString(s) * 4 // What?
By definition, a procedure would then be a statement.
Blurring the Lines
Like with expressions and statements, modern programming languages have blurred the lines between functions and procedures. In some cases, it’s not even possible to separate the two.
Consider Java which has a pass-by-value system. If we want to design a data structure, we often implement actions like add
, remove
, push
, pop
, enqueue
, dequeue
, etc. These actions are intuitive because they work how we expect them to work. For example, if we want to add an element to a stack, we’re going to call push
with a copy of the element as input.
Now, imagine we want to implement one of the remove methods (i.e. pop
). How do we go about doing it without blurring the lines between function and procedure? Clearly, pop
has a side effect: it removes the top element from the stack. Ideally, however, we’d also like to be able to return that value. Since Java is pass-by-value, we can’t pass a reference to the element back to the caller through one of our parameters. In other words, we’re stuck creating a function with side effects.
As a consequence, our pop
method could be used as either an expression or a statement. When used in an expression, it suddenly becomes difficult to reason about what that expression is doing because parts of that expression may see different states of the stack. In addition, successive calls to the same expression may yield different results as the state of the stack changes each call.
That said, there is one way around this problem. We could create a pair of methods, one function and one procedure, to get the top element from the stack (peek
) and remove that element (pop
). The idea here is that we maintain the separation between pure functions and procedures. In other words, we can use peek
when we want to know what value is on the top of the stack without modifying the stack. Then, we can use pop
to remove that top element.
Of course, introducing a pure function and a procedure in place of a function with side effects requires a bit of discipline that may or may not pay off. It’s up to you to decide if it’s worth the effort.
Discussion
For me, learning about the distinction between statements and expressions set off a chain reaction of questions about language design. After all, millions of people around the world are coding without any concern for these details, so my question is: does it really matter?
Lately, I’ve noticed a trend toward functional programming (FP), and I wonder if that’s a consequence of all the technical debt that’s built up from the blurred lines between expressions and statements. If not, is this trend toward FP really just hype? After all, FP isn’t new. For instance, Lisp is over 60 years old which is eons in the tech community. What are your thoughts?
While you’re here, check out some of these related articles:
Also, if you’re interested in growing the site, I have a mailing list where you’ll get weekly emails about new articles. Alternatively, you can become a full blown member which will give you access to the blog. At any rate, thanks for taking some time to read my work!
Edit: Back when I used to have comments enabled on this blog, someone shared some kind words:
Recent Posts
Teaching at the collegiate level is a wonderful experience, but it's not always clear what's involved or how you get there. As a result, I figured I'd take a moment today to dump all my knowledge for...
It's been a weird week. I'm at the end of my degree program, but it's hard to celebrate. Let's talk about it.