Guarantees on order of evaluation?

Are there any guarantees on the order in which sub-parts of expressions (e. g. function arguments) are evaluated? It seems like Julia evaluates from left to right, but there doesn’t actually seem to be anything in the documentation about this? The only thing I have been able to find is that the order of evaluations in a chained comparison is not defined.


Background: I have some code that looks like this

sum(
	factor1(i, j, k) * (
		factor2 = A(i, j, k) * B(i, j, k) * C(i, j, k)
	) *
	(factor2 == 0 ? 0 : expensive_function(i, j, k))
	for i, j, k in Iterators.product(gen(args)...)
)

and would like to make sure that the assignment and use of factor2 in the same expression will always work properly.

In other words, I’m trying to optimize a calculation to stop early when some factor is 0. So what I really want is short-circuit evaluation for multiplication, which doesn’t seem to exist? If there is a better way to achieve this, I’d be interested as well! (And yes, I realize that I could rewrite the sum(…) as a normal for loop, but I prefer the “functional” form.)

function f(i, j, k)
    factor2 = A(i, j, k)*B(i, j, k)*C(i, j, k)
    iszero(factor2) && return factor2
    return factor1(i, j, k)*factor2*expensive_function(i, j k)
end
sum(splat(f), Iterators.product(gen(args...)))
1 Like

Why not something more like this?

sum(
    let factor2 = A(i,j,k) * B(i,j,k) * C(i,j,k)
        if iszero(factor2);  factor2
        else  factor1(i,j,k) * factor2 * expensive_function(i,j,k)
        end
    end
    for (i,j,k) in Iterators.product(gen(args)...)
)

Another possibility:

sum(Iterators.product(gen(args)...)) do (i,j,k)
    factor2 = A(i,j,k) * B(i,j,k) * C(i,j,k)
    iszero(factor2) && return factor2
    factor1(i,j,k) * factor2 * expensive_function(i,j,k)
end
1 Like

To answer your original question:

https://docs.julialang.org/en/v1/manual/mathematical-operations/#Operator-Precedence-and-Associativity

2 Likes

Positional and keyword arguments are evaluated left to right. I’m not sure where this is clearly documented, though — see the discussion in Document function argument evaluation order by yurivish · Pull Request #24603 · JuliaLang/julia · GitHub, which was never completed.

4 Likes

Although, I think it should be said that the order of evaluation of function arguments seems like a leaky abstraction at best, and (subjectively, open to disagreement) I think it would usually be bad style to design code that depends on this behavior for performance. I think it will be more clear to the reader, and possibly less susceptible to future confusing performance regressions during a refactor, if you build the inputs as separate objects (possibly in a let block) in the order you want before passing them into the function.

Slick one-liners are fun, but so is obviousness and durability :slight_smile:

1 Like

I believe everything is evaluated left-to-right (that’s a guarantee except if you control with parenthesis, and also precedence rules apply), except for, yes (there’s a good reason for this, to allow it undefined, but I think in practice for now, if may also be left-to-right):

The only thing I have been able to find is that the order of evaluations in a chained comparison is not defined.

I recall however one suggestion to break that rule for 2.0 (may never be implemented, meant for matrices if I recall, the title is misleading, doesn’t justify this issue):

You can also change to more optimal way of evaluating with a macro (for matrix multiplies, then data dependent, I forget the package that implements it, maybe those in the following discussion, except they are outdated).

So what I really want is short-circuit evaluation for multiplication 3, which doesn’t seem to exist?

Seems would be doable with a macro.

There is documentation of the evaluation order for keyword arguments:

Keyword argument default values are evaluated only when necessary (when a corresponding keyword argument is not passed), and in left-to-right order. Therefore default expressions may refer to prior keyword arguments.

Thanks, but that only explains how things are parsed when different operators are mixed, not the order in which the arguments to each individual operator (or function) are evaluated. Note that the example I posted contained only multiplications, so the question of operator precedence doesn’t apply.

That’s very useful! Indeed it seems like this was meant to be documented, but has somehow fallen by the wayside.

I’m not sure what you mean by it being a leaky abstraction – things have to be done in some order (although this doesn’t have to be stable or consistent – my question is just whether it is or not), and for expressions with side effects, the order does matter (as in my example). I don’t really see an abstraction here beyond the basic “expressions” and “function calls”. I think the code I posted is perfectly clear; I’m just asking whether it can be expected to work :wink:

I’ll think about a let (or even begin) block à la @uniment or a macro (@Palli), though.

I don’t think this would’ve changed the order of evaluation of function arguments, considering that a * b * c is (currently) parsed as *(a, b, c). The * function can do to its arguments whatever it wants; I’m asking about what happens if a, b and c are expressions with side effects.

Indeed – since nobody seems to have done it so far, I might just write one :smiley:

Technically, this only describes the order for evaluation for keyword argument defaults :wink:

1 Like

E.g. a(b(c), d(e)) + f(g) would evaluate b then d, then a (still understood as left-to-right, but not strictly as in e.g. SmallTalk), then f, but with the 2.0 braking change I mentioned, then first f, if I understood things.

OK, now I’m confused.

  1. The part you’re quoting was a reply to CameronBieganek.
  2. Your example a(b(c), d(e)) + f(g) should be identical regardless of the current parsing or the one proposed in the issue you linked, since there is only one +. In both cases, it would parse to +(a(b(c), d(e)), f(g)) and be evaluated like any function with 2 arguments.

Sorry, I think I meant to write a(b(c), d(e) + f(g))

As long as there is only one +, there’s no difference either way?