Tips and tricks in Julia

2022

This post is some kind of a personnal collection of things learnt on-the-fly, small tips and tricks in the Julia language. They're not life-safers and won't make your code work better nor faster, but they'll probably help you write cleaner code, they're incredibly smooth, and using them fills me with an intense satisfaction. I hope this helps.

Contents

REPL tricks

Typing ] in a REPL brings you to package mode – you can activate environments or add packages.

Typing ? in a REPL brings you to help mode – type anything and you'll get documentation.

Typing ; in a REPL brings you to shell mode – you can do your regular ls and cd and grep commands.

To come back to the normal mode, juste type del.

Writing the output to the REPL

If you're in a REPL and you want to define something, say an array, the ouput will be written in the REPL. In other words, you'll see

julia> x = rand(2)
2-element Vector{Float64}:
0.35720058578070635
0.5348958549457257

If you don't want the output to be displayed, you only have to add ; at the end of the line. Also, you can chain many expressions in the same line if you separate them with ;.

julia> x = rand(2);y = 2*x;

Updating Julia

Julia evolves fast. Right now we're at 1.8x, but 1.9 is in alpha and scheduled soon. To upgrade my local julia version, I always use Abel Siqueira's Jill installer. Basically, you just have to

Multiple dispatch

Multiple dispatch is when a function has several definitions according to the type of its arguments. Typically,

function f(x::Int)
x+1
end

function f(x::String)
string(x, "+1")
end

Here, the function $f$ is said to have two methods and you can see all the methods of $f$ and where they are defined with methods(f). You can also add some kind of constraints on the parameters: for instance, if you want a function to take as input two elements of the same type T, whatever it is, then

function f(a::T, b::T) where {T}
return "whatever"
end

Inlining

Inlining refers to the practice of replacing a function call by its body. Instead of calling the function (which wastes a small amount of overhead), the compiler directly uses the code used to define the function. Under certain circumstances, this increases the speed of your program because 1) you lose the overhead time of the function call and 2) the compiler can further optimize inlined expressions which could not have been optimized otherwise.

Inlining optimization is in general a subtle technique; sometimes it can even fail, for example when you inline huge portions of code. The best practice is to time and benchmark your code to see what works better, see this post by Shuhei Kadowaki.

In Julia, if you want to tell the compiler to inline a function, you can do this using the @inline macro. Apparently, Julia automatically inlines small functions, so you should use this macro for slightly bigger functions, so if you want to forbid Julia to inline them you can use @noinline.

Logging macros

Most people debug their code by gently sprinkling println statements all around their code. In Julia, some very useful macros allow you to do this in a more classy style: they are @info, @warn, @error, @show, @debug. You use them by providing variables or key = values pairs. For example,

julia> x = rand()
0.5541313526244116

julia> @info "This is an info message" x y = x^2
┌ Info: This is an info message
│   x = 0.5541313526244116
└   y = 0.30706155596136

The macros @warn, @error, ... are similar. At first sight, one might think that these macros are just more specific versions of print statements, but in reality they are internally endowed with metadata which interact with the Logging package.

When you call one of these macros, a log event happens. The metadata associated to a log event are: the source module from which the event comes from, the file, the line, an ID and some extra info. Julia's default logger then chooses to display these metadata based on various things. For example, an @info event will display the dictionnary you gave as argument, but not the file or the module, while an @error message will also display the line:

julia> @info "This is an info message"
[ Info: This is an info message

julia> @error "Something just broke"
┌ Error: Something just broke
└ @ Main REPL[4]:1

This example was written in a REPL, hence the line number 1.

This is good and usually sufficient for one's need, but we only tickled the full power of the Logging functionalities in Julia. More can be found on the JuliaLogging excellent webpage.

Saving stuff to files

You did some experiments, got a nice result under the shape of - say - an array, and you want to keep it somewhere for later. You can of course use the good old write function, but at the moment it's way better to use the BSON.jl package. This package encodes nearly everything using the Binary JSON format and is really easy to use thanks to the utility functions @load, @save.

Suppose that you have an array of floats to save, and a string:

arr = rand(10)
phrase = "Vote for Pedro"

Then you can both save them in the file "output.bson" with

using BSON:@save, @load
@save "output.bson" arr, phrase

The file is created or rewritten. Later, when you want to load those variables, you only have to do

using BSON:@load
@load "output.bson" arr, phrase

and a variable phrase is created with the value you stored last time. Indeed, you don't need to load all the variables you saved. If you only need to work on the string phrase you can just do

@load "output.bson" phrase

Keyword arguments

It can be a good programming practice to wrap your function arguments inside an Args structure. For instance, instead of having

function f(size, depth, epsilon, tolerance, number_iterations)
# do stuff using these args

one could simply define a type for the arguments and the using it in the signature of the function:

mutable Struct Args
size
depth
epsilon
# etc
end

function f(args::Args)
# do stuff with args.size, args.depth, etc

A very nice way of doing this is to use the Base.@kwdef macro, which requires you to use the keywords when instanciating the structure. Taken from the doc itself:

julia> Base.@kwdef struct Foo
a::Int = 1         # specified default
b::String          # required keyword
end

julia> Foo(b="hi")
Foo(1, "hi")

Type stability

A function should always return values of the same type.

This great example is taken from the official doc. Suppose you have a function like pos(x) = x < 0 ? 0 : x. You did not specify any types, so Julia needs to infer the types by itself. But here, if x is positive, the output is x (say, a Float32) and if x is a negative float, the output is 0, that is...

julia> x = -3.0
julia> typeof(pos(x))
Int64

The output is an Int. The function pos is not type stable – shame! The solution, here, is pos(x) = x < 0 ? zero(0) : x. In general, if you have an object y and you want to convert it to the type of x, this operation can be performed (if able) by oftype(x,y). There are helper functions, like one(x) which returns a unit of the same type of x.

Coding style

Most of Julia's packages adopt the BlueStyle guideline in addition to the general guidelines. In general, you can check that your files have a correct style by using JuliaFormatter.jl: you just go at the root of your repo and launch format("."). All your files will be formatted. In VSCode, you can simply right-click on your file in the text editor and select Format Document.