Introduction

Recently, I was confused by how Julia parser works and complained on Julia Slack about it (in a moment I will explain what confused me). Then I learned Miguel Raz Guzmán Macedo has a very nice post about surprising behaviors of Julia, so today I thought to promote Miguel’s blog :).

In my post, not to steal all the fun you will have when reading Miguel’s blog, I will write about Julia’s behavior that surprised me and three behaviors, related to operator precedence, that commonly lead to bugs.

The post was written under Julia 1.7.

What surprised me

The behavior of Julia that caught me off guard is:

julia> -a = 10
- (generic function with 1 method)

As you can see, by accident instead of writing -a == 10 I have written -a = 10. In consequence instead of doing an equality test we have defined a new function for the - operator in module Main overshadowing the - definition from the Base module, as you can see here:

julia> -(50)
10

julia> 1 - 2
ERROR: MethodError: no method matching -(::Int64, ::Int64)
You may have intended to import Base.:-
Closest candidates are:
  -(::Any) at REPL[1]:1

The reason of this behavior is that for Julia’s parser writing -a = 10 means the same as writing -(a) = 10, which, can be recognized as a one-line function definition syntax.

Why is this behavior problematic? Once you have defined a new function for - in Main you have two options. Either restart your REPL or do - = Base.:- to bind Base.:- with - defined in Main (I would recommend restarting REPL instead of doing the work-around).

Operator precedence corner cases

Here are three cases of operator precedence surprises in Julia.

Scenario 1: & and |

When you write:

julia> 1 == 3 & 1 == 1
true

instead of expected false you get true. The reason is that you probably thought that the parser will interpret your expression as:

julia> (1 == 3) & (1 == 1)
false

While Julia interprets it as:

julia> 1 == (3 & 1) == 1
true

Scenario 2: ranges

When you write:

julia> 1:2 .+ 3
1:5

you might have expected:

julia> (1:2) .+ 3
4:5

but actually this is interpreted as:

julia> 1:(2 + 3)
1:5

Scenario 3: pairs and anonymous functions

When you write:

julia> :a => x -> x => :b
:a => var"#1#2"()

you probably expect:

julia> :a => (x -> x) => :b
:a => (var"#3#4"() => :b)

but in reality you get:

julia> :a => (x -> x => :b)
:a => var"#5#6"()

The last scenario is relevant in DataFrames.jl, where we often use syntax:

source_column => (x -> some_anonymous_function_body) => target_column_name

Conclusions

As any programming language Julia has some syntax corner cases that can be surprising. The problems with operator precedence I have listed in this post have a simple practical solution: if you are unsure about operator precedence be explicit and use parentheses to clearly signal how you want your expression to be evaluated.