First Contact with Julia: Generated Functions, Dispatch, and Why the Language Exists

A video on Julia's @generated functions turned into a REPL session proving every claim by hand

Started from a video on @generated functions, ended up installed-and-verified in the REPL, then zoomed all the way out to “what is this language even for.” Reference notes below, written so a future session can pick this back up without re-watching anything.

Why this exists

Julia showed up as a tangent while learning BHP and AWS material, not the main track. Worth a dedicated note specifically so it doesn’t get lost — this is a first-contact session, not mastery, and the whole point of writing it down is to have something to return to instead of re-deriving it from scratch.

Part 1 — What a generated function actually is

A regular Julia function runs its body every time it’s called. A function marked @generated is different: its body runs once per unique combination of argument types, and instead of returning a normal value, it must return a Julia Expr — quoted code. That returned expression is what gets compiled into the real, runtime method for that specific type combination.

Two completely separate phases, and they never mix:

  • Generation time — runs once per type combination. Has access to the argument types as real Julia type objects. Can branch on them, compute with them, inspect them (field count, subtype checks, etc.). Cannot see the actual runtime values.
  • Runtime — the compiled method executes with real values, at full native speed, same as any ordinary compiled function. No dispatch overhead, no re-generation, because the type combination was already seen.

The three syntax rules: @generated goes directly before function; type parameters are declared with a where T clause (the bridge between the caller’s runtime types and the generation body); the body must return an Expr object — built via a quoted block :( ... ), a quote ... end block, or manually via Expr(:call, :+, :x, :x).

Part 2 — Proving it in the REPL

Confirmed Julia was already installed: julia --version1.12.7, managed via juliaup (/home/lvydvy/.juliaup/bin/julia). Launch with a bare julia command, exit with exit() or Ctrl+D.

Defined the video’s simplest example:

julia

@generated function twice(x::T) where T
    return :(x + x)
end

twice(5)10 (Int64 path). twice(2.5)5.0 (Float64 path, separate generated method). Calling twice(5) again returned 10 with no regeneration — same cached method reused.

Proved the caching claim isn’t just documentation, with a version that prints during generation:

julia

@generated function twice2(x::T) where T
    println("generating for ", T)
    return :(x + x)
end

twice2(5) printed generating for Int64 once, then 10. Calling it again with the same type printed nothing — straight to the cached method. A new type (2.5) triggered a fresh generating for Float64 line. That’s the memoization guarantee made concrete rather than taken on faith.

Then demystified what Expr actually is:

julia

dump(:(x + x))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Symbol x
    3: Symbol x

That’s the whole trick laid bare — x + x isn’t magic, it’s a three-element array wrapped in a struct: “this is a call,” to +, with arguments x and x. Generated functions are just ordinary Julia code that builds one of these trees and hands it back.

One nuance worth remembering: the $ inside a quoted expression is expression interpolation, not string interpolation — it splices an actual generation-time value (like a type object or a computed constant) directly into the expression tree before compilation, which is how generated code bakes in things like a tuple’s field count as a literal rather than recomputing it at runtime.

Part 3 — Zooming out: what Julia is for

Built at MIT (~2009–2012) to solve the “two-language problem” — prototype in something Python/R-friendly, then rewrite the hot path in C/Fortran for speed. Julia’s bet: write it once, get near-native performance anyway, because Julia JIT-compiles each function the first time it’s called with a given set of argument types (LLVM under the hood). That’s the same mechanism @generated functions hook into directly — Julia was already doing type-specialized compilation; generated functions just let you influence what gets compiled.

The other defining trait is multiple dispatch as the core paradigm instead of classes/methods: which method runs depends on the types of all the arguments, not just the first one the way typical OOP method calls work. Very natural fit for math-heavy code.

Who uses it, and why: mostly people doing numerical, scientific, or data-heavy work where Python got too slow to iterate on directly. Notable areas — climate modeling (NASA-affiliated CliMA project), differential equations and simulation (DifferentialEquations.jl is considered one of the strongest solver suites in any language), optimization and operations research (JuMP.jl), quantitative finance, pharma/biotech modeling, and a growing presence in scientific machine learning (Flux.jl, physics-informed neural nets). The Federal Reserve has used it for economic modeling. Smaller community than Python’s, concentrated wherever raw numerical performance and mathematical expressiveness both matter.

Part 4 — Basic syntax tried in the REPL

julia

# plain function, no type annotations required
function area(radius)
    return π * radius^2
end
area(3)  # 28.274333882308138

# multiple dispatch — different methods per argument type
speak(x::Int) = println("an integer: ", x)
speak(x::String) = println("a string: ", x)
speak(42)     # "an integer: 42"
speak("hi")   # "a string: hi"

# arrays and broadcasting — very idiomatic Julia
v = [1, 2, 3, 4]
v .^ 2         # [1, 4, 9, 16], element-wise, no explicit loop
sum(v .^ 2)    # 30

The .^ broadcasting syntax vectorizes any function/operator over a collection while still compiling to a tight loop — not an interpreted list-comprehension-style penalty.

Queued for next session — not yet run

Introspection tools identified but not yet exercised on the plain-function examples:

julia

dump(:(function area(radius) return π * radius^2 end))
dump(:(speak(x::Int) = println("an integer: ", x)))
dump(:(v .^ 2))          # broadcasting desugars to a regular call — worth seeing

methods(speak)            # the dispatch table itself
@which speak(42)           # which method a given call actually resolves to

@code_lowered area(3)      # Expr-based AST, pre type-inference
@code_typed area(3)        # same code with every inferred type filled in

The @code_lowered / @code_typed progression is the same idea as @generated internals — just observing what the compiler does with an ordinary function instead of steering it directly.

Summary table

ConceptWhat it meansProved how
@generated functionBody runs once per type combo, returns Expr instead of a valuetwice/twice2 definitions
Generation vs runtimeTwo separate phases; generation only sees types, not valuesprintln inside twice2‘s generation body
Caching/memoizationSame type combo never regeneratesRepeated twice2(5) calls, no repeat print
Expr objectsQuoted code is just a small tree (head + args)dump(:(x + x))
Multiple dispatchMethod choice depends on all argument typesspeak(::Int) vs speak(::String)
Broadcasting.^ vectorizes without an explicit loop, desugars to a plain callv .^ 2, queued dump() check
Why Julia existsPython-like syntax, C-like speed via JIT + LLVMjulialang.org overview, multiple dispatch example

Next step

Read more of the official docs (docs.julialang.org, the Manual section) before the next hands-on session, then come back and run the queued introspection commands above.