Lecture 38: Introduction to Julia#

Note

This lecture introduces the Julia Programming Language, focusing on its core design principles and practical use in scientific computing. We will cover installation, environment setup, basic syntax, and unique features such as multiple dispatch and type stability. By the end of this lecture, you should be comfortable writing and running simple Julia programs, managing packages, and understanding why Julia is positioned as a high-performance companion to R and Python in data science and engineering workflows.

Why Julia#

ulia combines the readability of high‑level languages with near‑C performance via JIT compilation and multiple dispatch. It is designed for numerical and scientific computing, making it a strong companion to R and Python in this course.

Verifying#

julia --version

You should see something like julia version 1.x.y.

Installing VS Code#

VS Code + Julia extension#

  1. Install VS Code (Windows/macOS/Linux).

  2. From Extensions panel, install Julia (by Julia Computing).

  3. Point the extension to your Julia binary if needed:

    • VS Code → Settings → search Julia: Executable Path

    • Example path (macOS via juliaup): ~/.juliaup/bin/julia

  4. (Optional) Install CodeLLDB for debugging.

Quality-of-life extensions#

  • Inline Inlay Hints and Error Lens (already bundled in Julia ext.)

  • Jupyter if you prefer notebooks (.ipynb) in VS Code

REPL workflow#

  • Open a .jl file → Alt+J then Alt+O (or Command+J on macOS) to open REPL

  • Send line/selection with Shift+Enter

Hello World!#

# Hello World in Julia
println("Hello, CE5540!")

Data Types in Julia#

Common scalar types:

  • Int64, Float64, Bool

  • Char, String

  • Missing, Nothing

  • ComplexF64, Rational{Int}

Use typeof(x) to inspect types.

# Character & String
x = 'J'                 # Char
s = "CE5540"            # String
println("x = ", x, " :: ", typeof(x))
println("s = ", s, " :: ", typeof(s))

# Integers & Floats
i = 42                  # Int64 (on 64-bit platforms)
r = 3.14                # Float64
println("i :: ", typeof(i), ", r :: ", typeof(r))

# Boolean
b = i > 40
println("b = ", b, " :: ", typeof(b))

# Complex & Rational
c = 2 + 3im
q = 3 // 7
println("c = ", c, " :: ", typeof(c))
println("q = ", q, " :: ", typeof(q))

# Missing / Nothing
m = missing
n = nothing
println("missing :: ", typeof(m), ", nothing :: ", typeof(n))

Data Structures in Julia#

  • Tuple (immutable), NamedTuple

  • Vector/Matrix (1D/2D Array), range objects

  • Dict (hash map), Set

  • DataFrame (from DataFrames.jl)

Note: Many operations are not automatically vectorized; prefer broadcasting with the dot . operator (e.g., sin.(x)).

# Tuples and NamedTuples
t  = (1, 2.0, "a")
nt = (a = 1, b = 2.0, c = "a")
println(t, " :: ", typeof(t))
println(nt, " :: ", typeof(nt))

# Arrays (Vector/Matrix) and ranges
v = [1, 2, 3, 4]             # Vector{Int}
A = [1 2 3; 4 5 6]           # 2×3 Matrix{Int}
r = 0:2:10                   # range 0,2,4,6,8,10
println(v, " :: ", typeof(v))
println(A, " :: ", typeof(A))
println(collect(r), " :: ", typeof(r))

# Dict and Set
D = Dict("a" => 1, "b" => 2)
S = Set([1,2,2,3,3,3])
println(D, " :: ", typeof(D))
println(S, " :: ", typeof(S))

# Broadcasting with dot notation
x = [1.0, 4.0, 9.0]
y = sqrt.(x)    # element-wise sqrt
println(y)

Control Flow#

Julia supports standard if/elseif/else, for, and while constructs.

# If / elseif / else
x = 10
if x > 10
    println("x > 10")
elseif x == 10
    println("x == 10")
else
    println("x < 10")
end

# For loop
s = 0
for k in 1:5
    s += k
end
println("Sum 1..5 = ", s)

# While loop
i = 1
prod_ = 1
while i <= 5
    prod_ *= i
    i += 1
end
println("Product 1..5 = ", prod_)

Writing Functions in Julia (and Multiple Dispatch)#

Functions can be type-annotated.

# Factorial (iterative)
function factorial_iterative(n::Integer)
    n < 0 && error("n must be non-negative")
    result = one(n)
    for k in 2:n
        result *= k
    end
    return result
end

# Factorial (recursive)
function factorial_recursive(n::Integer)
    n < 0 && error("n must be non-negative")
    return n  1 ? one(n) : n * factorial_recursive(n-1)
end

println("5! iterative = ", factorial_iterative(5))
println("5! recursive = ", factorial_recursive(5))

Multiple Dispatch#

Julia selects methods at runtime based on the types of arguments.

# Multiple dispatch example
f(x::Int, y::Int) = x + y
f(x::Float64, y::Float64) = x + y
f(x, y) = string(x, y)   # fallback (String concatenation)

println("f(2, 3) -> ", f(2,3), " :: ", typeof(f(2,3)))
println("f(2.0, 3.0) -> ", f(2.0,3.0), " :: ", typeof(f(2.0,3.0)))
println("f("a", 3) -> ", f("a", 3), " :: ", typeof(f("a",3)))

# Method lookup with @which (reveals the exact method that will be called)
# Try with our multiple-dispatch function f:
@which f(1, 2)          # both Int → f(x::Int, y::Int)
@which f(1.0, 2.0)      # both Float64 → f(x::Float64, y::Float64)
@which f(1, 2.0)        # promotes to Float64 → f(x::Float64, y::Float64)

# Works for Base methods too
@which +(1, 2)          # shows which method of + is used
@which *(1.0, 2)        # multiplication method for Float64 × Int

Type Stability#

# Type stability matters for performance. A function is *type-stable*
# if the compiler can infer a concrete return type from the input types.

# Type-stable example
f1(v::Vector{Int})::Int = sum(v)

# Type-unstable example: operates on abstractly-typed inputs
f2(v::Vector{Any}) = sum(v)

# Construct two vectors
v_stable = [1, 2, 3, 4, 5]                  # Vector{Int}
v_unstable = Any[1, 2.0, 3, 4.0, 5]         # Vector{Any} (mixed types)

# Inspect type inference
@code_warntype f1(v_stable)     # should show a concrete return type (Int64 on 64-bit)
@code_warntype f2(v_unstable)   # likely to show `Any`-typed intermediate/return

# Tips for stability:
# - Use concrete container element types (Vector{Float64}, Vector{Int}, etc.)
# - Avoid changing the type of a variable inside a function
# - Annotate return types where it clarifies intent (not always necessary)

Writing Fast Julia: Type Stability & Broadcasting#

  • Type stability: Ensure functions return a consistent type. Use @code_warntype in the REPL to diagnose instabilities.

  • Avoid global state: Wrap code in functions; globals are slow.

  • Use broadcasting: Prefer f.(x) over manual loops when applying scalar functions element-wise.

  • Preallocate: For large loops, preallocate arrays to avoid repeated memory allocations.

Creating a Julia Project#

# Julia environments and package management with Pkg
# Run in the Julia REPL or VS Code Julia REPL

import Pkg

# 1) Create/activate a new project (creates Project.toml and Manifest.toml)
Pkg.activate("ABM101")  # local env in ./ABM101
# Alternatively: Pkg.generate("ABM101")  # scaffolds a new package project

# 2) Add packages
Pkg.add([
    "DataFrames",
    "CSV",
    "Plots",
    "Distributions",
    "StatsBase"
])

# 3) Check status / resolve
Pkg.status()
Pkg.resolve()

# 4) Pin (optional), update, and instantiate (recreate exact environment on new machine)
# Pkg.pin("DataFrames")
Pkg.update()
Pkg.instantiate()

# 5) Using packages in code (after activation)
using DataFrames, CSV, Plots, Distributions, StatsBase

# 6) Best practice for reproducibility:
#    - Commit Project.toml & Manifest.toml to version control
#    - Colleagues call `Pkg.activate("."); Pkg.instantiate()` to match your environment