cv.cm

Coding · Programming languages

Julia tutorial for beginners

6 min read

Short answer

Julia is a high-level, dynamically typed language designed for fast numerical and scientific computing. It reads like Python or MATLAB but compiles each function to native code the first time it is called, so loops can run at speeds close to C. Its central idea is multiple dispatch: a function picks its method based on the types of all its arguments. Install it with juliaup and run a file with julia hello.jl.

What Julia is and who uses it

Julia was created by Jeff Bezanson, Stefan Karpinski, Viral Shah and Alan Edelman, and it became public in 2012. Version 1.0 arrived in 2018, and the 1.x series keeps backward compatibility. Julia aims to solve the "two-language problem": prototyping in a slow, easy language and then rewriting the hot parts in C or Fortran.

Julia is used by scientists, engineers and data analysts for simulation, differential equations, optimization, statistics, machine learning and high-performance computing. Its package ecosystem covers data frames, plotting, GPU computing and automatic differentiation.

Install Julia and run Hello, world

The recommended installer is juliaup, which installs and updates Julia versions on Windows, macOS and Linux (see julialang.org/downloads). After installation, julia starts the REPL and julia file.jl runs a script.

# hello.jl
println("Hello, world!")
julia hello.jl
# Hello, world!
julia
# julia> 1 + 2
# 3

In the REPL, press ] to enter package mode (for example add DataFrames), ? for help on a name, and ; for a shell. Press Backspace to return to the normal prompt.

Variables and basic types

Variables need no declaration, but every value has a concrete type, which you can inspect with typeof. You can add type annotations to function arguments and struct fields when you want them.

TypeExampleNotes
Int6442Default integer on 64-bit systems; overflow wraps around
Float643.14, 1e-3Default floating point
Rational1//3Exact fraction
String"hello"UTF-8; interpolate with "$name"
Char'a'A single Unicode character
Booltrue, falseLogical values
Symbol:nameAn interned name, often used as a key or option
Nothingnothing"No value", returned by functions with no result
name = "Ana"
age = 30
println("$name is $age, next year $(age + 1)")
# Ana is 30, next year 31
typeof(age)   # Int64
7 / 2         # 3.5  (always a float)
7 ÷ 2         # 3    (integer division, also div(7, 2))
2^10          # 1024

Julia accepts Unicode names such as π or α; in the REPL and most editors you type \pi and press Tab to get π.

Control flow

Blocks end with the keyword end; indentation is for readability only. if, for and while work as in most languages, and comprehensions build arrays in one line.

function sign_name(n)
    if n > 0
        "positive"
    elseif n < 0
        "negative"
    else
        "zero"
    end
end
for i in 1:3
    println("$i squared is $(i^2)")
end
squares = [x^2 for x in 1:5 if isodd(x)]   # [1, 9, 25]
x = 5
result = x > 3 ? "big" : "small"           # "big"

1:3 is a range that includes both ends. The last expression in a function or if block is its value, so sign_name(-2) returns "negative" without an explicit return.

Functions

add(x, y) = x + y                 # one-line definition
function greet(name = "world"; punctuation = "!")
    return "Hello, $name$punctuation"
end
greet()                           # "Hello, world!"
greet("Ana", punctuation = ".")   # "Hello, Ana."
square = x -> x^2                 # anonymous function
map(square, [1, 2, 3])            # [1, 4, 9]
v = [3, 1, 2]
sort!(v)                          # v is now [1, 2, 3]

Positional arguments come before the semicolon and keyword arguments after it. By convention, a function whose name ends in ! modifies one of its arguments, such as sort! or push!, while sort returns a new array.

Arrays, tuples and dictionaries

StructureLiteralNotes
Vector[1, 2, 3]1-based, mutable, grows with push!
Matrix[1 2; 3 4]Spaces separate columns, ; separates rows
Tuple(1, "a")Immutable, fixed length
Named tuple(name = "Ana", age = 30)Fields accessed with .name
DictDict("a" => 1)Mutable hash map
SetSet([1, 2, 2])Unique values
v = [10, 20, 30]
v[1]          # 10   (first element)
v[end]        # 30
push!(v, 40)
A = [1 2; 3 4]
A * A         # matrix product: [7 10; 15 22]
A .* A        # element-wise:   [1 4; 9 16]
v .+ 1        # broadcasting:   [11, 21, 31, 41]
sqrt.([1, 4, 9])   # [1.0, 2.0, 3.0]

The dot syntax is called broadcasting: f.(x) applies f to every element, and .+, .* work element by element. Julia fuses chains of dotted operations into a single loop.

Signature feature: multiple dispatch

A Julia function can have many methods. When you call it, Julia chooses the most specific method for the types of all the arguments. This replaces class methods: types hold data, and behavior is added by defining methods for them, even from another package.

abstract type Shape end
struct Circle <: Shape
    r::Float64
end
struct Rect <: Shape
    w::Float64
    h::Float64
end
area(c::Circle) = π * c.r^2
area(r::Rect) = r.w * r.h
shapes = [Circle(1.0), Rect(2.0, 3.0)]
println(round.(area.(shapes), digits = 2))   # [3.14, 6.0]

Because Julia compiles a specialized version of a function for the argument types it sees, the first call of a function is slower (it includes compilation) and later calls are fast. Use mutable struct when fields must change after creation.

Error handling

Julia uses exceptions. Throw a built-in exception type such as ArgumentError or DomainError, or define your own as a struct that subtypes Exception. tryparse returns nothing instead of throwing.

function parse_age(s)
    n = tryparse(Int, s)
    n === nothing && throw(ArgumentError("not a number: $s"))
    n < 0 && throw(DomainError(n, "age must not be negative"))
    return n
end
try
    parse_age("abc")
catch e
    if e isa ArgumentError
        println("Error: ", e.msg)
    else
        rethrow()
    end
end
# Error: not a number: abc

A small complete program: word frequencies

text = "the cat and the hat and the bat"
counts = Dict{String,Int}()
for w in split(text)
    counts[w] = get(counts, w, 0) + 1
end
top = sort(collect(counts), by = last, rev = true)
for (word, n) in first(top, 2)
    println(word, " ", n)
end
# the 3
# and 2

split breaks the string on whitespace, get returns a default of 0 for new words, collect turns the dictionary into a vector of word => count pairs, and by = last sorts by the count.

Where to go next

  • The official Julia manual at docs.julialang.org, starting with "Getting Started".
  • The learning page at julialang.org, which lists tutorials, books and courses.
  • Read the "Performance Tips" chapter of the manual before optimizing code.
  • Explore packages such as DataFrames.jl, Plots.jl and DifferentialEquations.jl.

FAQ

Is Julia faster than Python?

For code with many loops and numeric work, Julia is usually much faster than pure Python because it compiles to native code. Python with libraries such as NumPy can be fast for vectorized operations, so the difference depends on the task.

Why does Julia start counting at 1?

Julia follows the convention of mathematics and of languages such as MATLAB, R and Fortran, where the first element of an array has index 1.

Why is the first run of my Julia code slow?

Julia compiles each function for the argument types the first time it is called. Later calls reuse the compiled code, so measure performance after a warm-up call or with a benchmarking package.

Is Julia free?

Yes. Julia is open source under the MIT license, and almost all of its packages are open source too.

All coding tutorials