This talk is inspired by the classic Wat talk by Gary Bernhardt, applied to Julia.
Huge thanks to Mason Protter, many of the inital specimens are his.
Why collect a huge array of scary footguns? Others have ranted before on all the things that are bad about Julia, profusely! Enthusiastically! I think there's good value in knowing precisely why you should hate your tools. I'm clearly in the "Julia will take over the world camp", and that effusiveness can work great for some projects, but it's good to understand the limitations of the tools we use. There's well known effective ways to come across in a reasoned manner when pitching Julia for a particular use case, but being able to specify many of these limitations and the pain points they inflict will generally show that you're willing to take criticism in a healthy manner.
As always, if you want to support me writing more of these Julia horror stories, please consider sponsoring me on GitHub.
TODO
and, or on empty collections
broadcasting shenanigans: (Credit to Mosè Giordano)
julia> all([] .== [42])
true
julia> all([] .≈ [42])
true
RNG seed set by @testset
: (Credit to Michael Abbott)
julia> @testset begin
x = rand()
@testset for i in 1:10
y = rand()
@test x == y
end
end;
Test Summary: | Pass Total Time
test set | 10 10 0.0s
Operator precedene with ranges: (Credit to Oscar Smith)
julia> -5:5 .+ .5
-5.0:1.0:5.0
julia> (-5:5) .+ .5
-4.5:1.0:5.5
Another few examples from Bogumił Kamiński's Blog "Confused by Julia" (which I will include for the completeness of this list, but leave you to visit his blog for the explainers)
julia> :a => x -> x => :b
:a => var"#1#2"()
julia> 1 == 3 & 1 == 1
true
var"N+1"
and other sneaky shenanigans like stealing the pipe operator with an even uglier syntax
struct PseudoClass{T}
data::T
end(o::PseudoClass)(f, args...; kwargs...) = f(o.data, args...; kwargs...)
var"'ᶜ" = PseudoClass
my_thing'ᶜ(stuff)'ᶜ(more_stuff, an_argument)'ᶜ(final_stuff; a_keyword_argument)
Courtesy of Stefan Karpinski
julia> e = 9998.0
9998.0
julia> 2e
19996.0
julia> 2e+4
20000.0
julia> 2e+5 # This should be 20001.0, and yet...
200000.0
julia> 2e + 5 # note the spacing
200001.0
Shadowing: Courtesy of Kristoffer Carlsson
julia> git_tree-sha1 = "8eb7b4d4ca487caade9ba3e85932e28ce6d6e1f8";
julia> 1-2
"8eb7b4d4ca487caade9ba3e85932e28ce6d6e1f8"
And another example:
julia> function f(x)
my_cool-variable=3
if x > 5
return my_cool_variable
else
return 3 - 1
end
end
f (generic function with 1 method)
julia> f(2)
3
julia> 3-1
2
Symbols and numbers: Courtesy of Mosè Giordano
:
julia> :a === "a"
false
julia> :2 === 2
true
Credit to Jakob Nybo Nissen
:
julia> :1234567890123456789 == 1234567890123456789
true
julia> :12345678901234567890 == 12345678901234567890
false
See also this issue.
As a corollary, a Pythonista stumper:
julia> arr1 = reshape(1.0:4.0, 2, 2)
2×2 reshape(::StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}, 2, 2) with eltype Float64:
1.0 3.0
2.0 4.0
julia> arr2 = zeros(2, 2)
2×2 Matrix{Float64}:
0.0 0.0
0.0 0.0
julia> arr2 .= arr1[:, :2]
2×2 Matrix{Float64}:
3.0 3.0
4.0 4.0
because :2
is not the same as 1:2
julia> arr2 .= arr1[:, 1:2]
2×2 Matrix{Float64}:
1.0 3.0
2.0 4.0
Callable ints by Alexander Plavin
: link here
julia> (1)(2)
2
# but
julia> x = 1
1
julia> (x)(2)
ERROR: MethodError: objects of type Int64 are not callable
BUT! This can be avoided, as Mason Protter
invokes through the magic of type piracy:
julia> (x::Int)(y) = x * y
julia> x = 1
1
julia> (x)(2)
2
and the following super dirty:
julia> (s::Symbol)(x) = getproperty(x, s)
julia> :im(1 - im)
-1
Lowering is hard, credit to Jonnie Diegelman
:
julia> nums = zeros(Int, 10);
julia> for nums[rand(1:10)] in 1:20
end
julia> nums
10-element Vector{Int64}:
12
16
7
20
19
18
15
0
13
17
(Python suffers from something similar). Explanation: As Jabon Nissen
pointed out, "It's because for i in 1:20 lowers to for i = 1:20 in Julia. Here, it's nums[rand(1:10)] = 1:20" This is another one for the road
julia> nums = [1, 3, 5, 7, 9];
julia> gen = (n for n in nums if n in nums);
julia> collect(gen)
5-element Vector{Int64}:
1
3
5
7
9
julia> nums = [1, 3, 5, 7, 9];
julia> gen = (n for n in nums if n in nums);
julia> nums = [1, 2, 3, 4];
julia> collect(gen)
2-element Vector{Int64}:
1
3
Why does this happen: Jeff points out that the thing to iterate over is evaluated once; everything inside has to be evaluated for each iteration and so can change. However, a cleverly place let
binding can avoid some of these headaches:
julia> gen = let nums = 1:2:9
(n for n in nums if n in nums)
end;
julia> nums = 1:4;
julia> collect(gen)
5-element Vector{Int64}:
1
3
5
7
9
isequal
vs egal
vs ==
vs ===
julia> first(1,2)
1-element Vector{Int64}:
1
Rationale: Partly explained in the docstring for first
. Maybe a MATLAB-ism.
julia> # Credit to Dheepak Krishnamurthy
julia> 1[1][1][1] == 1
true
What is this syntax?
julia> function (YOLO)
YOLO + 1
end
Credit to Miha Zgubič
julia> append!([1, 2, 3], "4")
4-element Vector{Int64}:
1
2
3
52
Explanation: convert
is called implicitly to make "4"
into Char
, and since Int('4') == 52
, you get the result above.
You can get similar results with
push!([1, 2, 3], '4')
x = [1, 2, 3];
x[3] = '4';
x
copyto!([1,2,3], "456")
Credit to Michael Abott
for those.
Not that the general Julia idiom of [x, y]
will try to promote to a common element type of possible, but only if it equals one of the input types. This is a constraint that homogenous array representation demands, but can lead to some interesting cases like: (Credit to MIlan Bouchet-Valat
)
julia> [BigInt[1], [1.0]]
2-element Vector{Vector}:
BigInt[1]
[1.0]
julia> [[1], [1.0]]
2-element Vector{Vector{Float64}}:
[1.0]
[1.0]
Credit to Vasily Pisarev
.
julia> countlines("""
Mary had a little lamb,
Its fleece was white as snow,
And every where that Mary went
The lamb was sure to go
""")
ERROR: SystemError: opening file "Mary had a little lamb,\n Its fleece was white as snow,\nAnd every where that Mary went\n The lamb was sure to go\n": No such file or directory
julia> threetuple = (3, 3.0, 3f0)
(3, 3.0, 3.0f0)
julia> threetuple isa NTuple
false
julia> threetuple isa NTuple{3,Number}
true
Both AgusThom @at_tcsc
has kindly corrected me on a non-exclusive to Python footgun:
julia> a = [[]]
1-element Vector{Vector{Any}}:
[]
julia> push!(a, a[1])
2-element Vector{Vector{Any}}:
[]
[]
julia> push!(a[1], "seriously?")
1-element Vector{Any}:
"seriously?"
julia> a
2-element Vector{Vector{Any}}:
["seriously?"]
["seriously?"]
where Agus' sinister variant also holds:
julia> a = fill([], 4)
4-element Vector{Vector{Any}}:
[]
[]
[]
[]
julia> push!(a[1], "seriously")
1-element Vector{Any}:
"seriously"
julia> a
4-element Vector{Vector{Any}}:
["seriously"]
["seriously"]
["seriously"]
["seriously"]
Hat tip to Unityper.jl devs and Jakob for pointing this one out to me.
julia> struct A end
julia> struct B
1 + 1
end
julia> methods(A)
# 1 method for type constructor:
[1] A()
@ REPL[5]:1
julia> methods(B)
# 0 methods for type constructor
Mark Kittisopikul