Apparent contradiction between Julia's short-circuiting rules and Julia's precedence rules


#1

I’ve found myself unable to explain the output of the following simple Julia program. It appears the output is totally dependent on whether one applies the short-circuiting rules or the precedence rules first. Short-circuiting is a minor optimization for a couple of operators whereas precedence affects all operators. Yet Julia seems to treat short-circuiting as the more important of the two. An example:

# Apparent conflict between precedence and short-circuit evaluation
# First define some helper functions
t(x)=(println(x);true)
f(x)=(println(x);false)
# Now comes the critcal expression
t(1) || f(2) && f(3)
# If one starts with the short-circuit rules, one would argue that || doesn't
# need to evaluate f(2) && f(3), because t(1) is true, giving an output
# of 1 \n true.  By actual experiment this is what Julia does.
# However, one could start with the precedence rules.  && has higher precedence,
# so it is evaluated first.  The fact that || doesn't need the result of && is
# irrelevant because we aren't evaluating ||.  f(2) ie evaluated and then by
# the short-circuiting of &&, f(3) is ignored.  So 2 \n is outputted and
# false is returned.  || now is the only remaining operator, so t(1) is
# evaluated, causing the output now to be 2 \n 1 \n and returning true.  Now
# the short-circuit rules claim we should ignore the value of && and return true
# as the final value.
# Julia apparently applies short-circuit before precedence.  Why??

#2

Short-circuiting is not an optimization. It’s part of the semantics of || and &&.
It also doesn’t contradict precedence. a || b && c is interpreted as a || (b && c) according to precedence rules, but according to short-circuit rules (b && c) is evaluated only if a evaluates to false.
This is the same behavior as in many other languages, including C, Java and Python.


#3

You should wrap in code blocks to avoid “shouting”


#4

Thank you to Michael and “yha” for their prompt replies.

First Michael: Sorry but this is the first time in several years I have used a list which needed to wrap code blocks. I’ll try not to shout again! My apologies.

Next yha: I have to respectfully disagree with some of your comments. The point I was trying to make with the comment about optimization is that short-circuiting is used because of the peculiarities of two specific operators. Technically you are correct in saying that short-circuiting is part of their semantics, but nobody would even care if short-circuiting didn’t optimize their run-time behavior.

I have to disagree with your statement that “It (short-circuiting) also doesn’t contradict precedence”. Your following sentence starts to indicate why, but you need to notice that the operands you call a, b, and c actually have side-effects (doing output). If precedence rules are followed, the evaluation of the “and” operator causes some output to occur. By the time you are checking the semantics (or anything else) about the “or” operator, the output may have already reached a physical device and hence CANNOT be ignored no matter what the semantics of the “or” operator are. The output is different depending on whether you pay attention first to precedence or short-circuiting.

The last sentence may be the real answer here. There are plenty of examples of ideas put into programming languages for no better reason than compatibility with older languages. For my personal purposes, I’m just going to continue to do what I’ve always done: don’t use short-circuit operators if there is any chance of side effects.

Again, thanks to both of you.


#5

These are the relevant facts:

  • && and || are both left associative.
  • && has higher precedence than ||.
  • && only evaluates the right expression if the left expression is true.
  • || only evaluates the right expression if the left expression is false.

The first fact is not actually relevant in this particular case but would be in longer chains. In this expression:

t(1) || f(2) && f(3)

Precedence implies that it’s this:

t(1) || (f(2) && f(3))

Since t(1) evaluates to true, f(2) && f(3) is not evaluated.


#6

Actually, most everybody would care because it would break a lot of code. These are common idiom in Julia:

is_ok(arguments) || error("invalid arguments)
x < 0 && break

etc.
It’s also common, both in Julia and other language to do things like if x != 0 && a_condition(a/x) or if x in collection && collection[x] == something
which relies on short-circuiting to avoid an error on the second operand.

No, it may not. I think you misunderstand what operator precedence means. It does not mean that in a || b && c, the expression b && c is evaluated first. It only means that it is evaluated as a || (b && c) rather than (a || b) && c. And the evaluation of a || (b && c) does not result in evaluation of (b && c) if a is true.


#7

Again, thank you for all who contributed! I think it is time to refocus on what the original question was. I understand that Julia does short-circuit evaluation on expressions involving the “and” and “or” operators because I actually ran the example I gave through Julia. I understand what short-circuiting does. As you may have guessed, I regard it as a bad idea but I don’t question that Julia use it.

My question is why the precedence rule stating that “and” has higher precedence than “or” isn’t applied before short-circuiting, contrary to what Julia does. Several responders seem to think that merely means inserting parentheses before short-circuiting, so let me suggest a simple and well-known experiment to show what I actually meant:

  1. Convert the expression in my example to postfix (prefix works too but for the sake of definiteness allow me to use postfix). The usual stack-based algorithm depends ONLY on the precedence of the operators but not on any short-circuiting, so you will get a clear indication of what happens when precedence is done first. The postfix conversion algorithm is a common method of understanding expressions in a way that is precedence and parenthesis-free, so this is a reasonable way to proceed.

  2. Now that all precedence has been eliminated we have completed the precedence-first part of my suggestion. Short-circuiting can easily be done on the resulting expression by an obvious algorithm.

If you try this you will get a different answer than Julia, yha, and Stefan get and IMHO it is the correct answer for doing the parsing “precedence-first”.

Hopefully in conclusion, I think yha’s answer that Julia’s developers wanted to maintain the same method of parsing expressions involving short-circuit operators as C, Python, and Java use is probably the best answer I’m going to get. I’ll just remember not to use terms with side effects in them. Again, thanks to everyone.


#8

As far as I understand, this is exactly what it means. Can you provide any source that says differently?

Or which programming languages behave differently?


#9

Please show some consideration to the users taking the time to read and discuss your post by formatting it properly. You can still edit it. The site uses CommonMark.


#10

It is. The whole postfix argument doesn’t make much sense to me and seems to lead to an incorrect conclusion, so appears to be unhelpful on the whole.

Or which programming languages behave differently?

As far as I’m aware they all work like this although some give && and || the same precedence which leads to left-to-right evaluation.


#11

There’s a reason why && and || appear in the Control flow chapter of the documentation — they’re not normal operators. They’re syntax. Unlike many other operators, you cannot use them as first-class functions. They cannot be extended with multiple dispatch. And perhaps most importantly, you cannot reference them independently or in prefix function call syntax, so your thought experiment doesn’t work in Julia.

If you want logical operators that don’t short circuit, you can use the bitwise & and |, but be aware that their precedences are tuned for bitwise arithmetic.

See also the difference between the ternary operator ? : and the ifelse function.


#12

The problem seems to be more with the understanding of precedence than with the short-circuiting rule.
Here is an example only about precedence:

julia> (println("hello"); 1) + (println("world"); 2) * (println("!"); 3)
hello
world
!
7

and in R

> {print("hello"); 1} + {print("world"); 2} * {print("!"); 3}
[1] "hello"
[1] "world"
[1] "!"
[1] 7

and in Python:

>>> def a(s, i):
...     print(s)
...     return(i)
... 
>>> a("hello", 1) + a("world", 2) * a("!", 3)
hello
world
!
7

So all the languages here know the precedence of * is higher than +, but they don’t evaluate (println("world"); 2) * (println("!"); 3) first, and the precedence really means inserting parentheses.