Re-posted from: https://bkamins.github.io/julialang/2022/03/04/wat.html
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.