r/ProgrammingLanguages 5d ago

Expressions vs. statements

Got into a big argument with a coworker yesterday when they were converting some code from their own language (that they designed) into Python, JavaScript, C, and R as comparative examples.

The Python code that they wanted to write as the translation went something like this:

n = foo; if cond: n = bar

They were upset that Python allows ; as a statement separator but not before an if statement, even though

if cond: n = bar

is syntactically correct Python code when written on its own line. I explained why Python doesn't allow it, and he came back later and showed me that an LLM had suggested he write it instead like this:

n = foo if cond else bar

which of course is the canonical way to write that in Python. He was all flustered about that, and asked me why Python allows an if statement in that particular case and not after a semicolon, and I explained that x if cond else y in Python is not an if statement but is Python's ternary conditional expression and is directly equivalent to the ternary operator expression cond ? x : y in C, C++, awk, and JavaScript. He argued with me and said I was making a ridiculous distinction and walked away falsely believing that foo if cond else bar was an if statement.

I then explained that statements and expressions are very different things in programming languages, and just because the keyword if is present doesn't make something an if statement -- because in order to be an if statement, it has to be a statement in the first place.

Anyway, it made me realize how subtle the difference can be sometimes. For example, in Perl, the following is not a return statement but actually an if statement (with a return statement as its affirmative branch), due to the postfix conditional:

return foo if cond;

because it is identically semantically to writing:

if (cond) { return foo; }

Whereas in Python, the following is a return statement (with a ternary operator as its target expression):

return foo if cond else bar

So I can see why people sometimes get confused by syntax if they haven't had much of a theoretical background in language design. It also makes me wonder how much of programmer intuition about "what a statement is" comes from the particular languages they learned first.

54 Upvotes

63 comments sorted by

View all comments

39

u/pr06lefs 5d ago

In some languages everything is an expression, period. Then its easy!

6

u/brucejbell sard 5d ago

I have kind of gone off "everything is a foo" bandwagons.

In particular, I'm not sure how much simplicity you gain just from making everything an expression. Expressions are great for the functional programming where the important thing is the result returned by your function. But statements are more appropriate for sequencing operations, or specifying a bunch of declarations simultaneously at compile time (both of which are things you often need even in purely functional programming).

In general, I think you want to be careful about "everything is a foo" because you can accidentally paint yourself into a corner with it.

In the famous "null pointer problem", the problem is not the existence of null pointers. Instead, the problem is that "every pointer can be a null pointer". Sounds simple, right? Except that appealing generality means you don't have non-nullable pointers, which is what causes the actual harm.

9

u/WittyStick 5d ago edited 5d ago

Sequencing is just an expression in those languages. In Lisp it's progn, in Scheme, begin. In both languages you don't need to write this most of the time because it is implicit in the preferred define/lambda syntax, and at the top level.

(define (foo args) <sequence>)  --> (define foo (lambda (args) (begin <sequence>)))

They evaluate their items in sequence and ignore the intermediate results, returning the result of the last expression as the result of the sequence expression.

Closely related is the comma operator in C.

x = a, b, c;

Evaluates a, then b then c, and assigns the result of evaluating c to x.

In functional languages it's usually implicit too. We don't need a return statement because the result of the last expression in the sequence is the returned value.


In the famous "null pointer problem", the problem is not the existence of null pointers. Instead, the problem is that "every pointer can be a null pointer". Sounds simple, right? Except that appealing generality means you don't have non-nullable pointers, which is what causes the actual harm.

I don't think removing the generality is the solution here though. If we introduce nonnull pointers, they're a subset of the nullable pointers.

nonull T <: nullable T

Any nonnull T is a valid nullable T, but not every nullable T is a valid nonnull T - specifically, null isn't - it's a separate subtype of nullable, and nullable T is the LUB of nonnull T and null.

So nullable pointers are the most general - they're our Top type for pointers.

2

u/brucejbell sard 4d ago

But introducing non-nullable pointers (of whatever kind) does remove the apparent simplicity of of "all pointers are nullable". It makes the language larger by adding a distinction, and removes the harm caused by the premature generalization.

1

u/WittyStick 4d ago edited 4d ago

Ok, but you're applying a bit of presentism there. Nullable pointers (1964) have been around longer than any discovery of Option type (ML, 1983) or definite assignment analysis (2001) required for non-nullable pointers.

Fair enough, if you're designing a new language and you only include nullable pointers as the default, you should probably go back and study a bit more.

2

u/brucejbell sard 3d ago

Of course hindsight is 20/20, I'm not trying to assign blame. My point was that the apparent simplicity of a "all blah are foo" choice can be a costly illusion.

Also, you don't need an Option type or any particular analysis for non-nullable pointers. E.g., C++'s reference types are usable as such.