Confused by Julia

By: Blog by Bogumił Kamiński

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.