Thinking in Julia#

By now you’ve met the pieces: types, dispatch, broadcasting, packages, macros, memory, performance. This chapter is different. It’s not about a new feature, it’s about the mindset that makes those features click into a coherent way of working. Learning a language’s syntax is easy; learning to think in it is what makes you fluent. Here are the habits I’ve internalized writing Julia, and that I wish someone had handed me on day one.

1. Write generic code, and let the compiler specialize#

This is the habit that took me longest to build, because it runs directly against the instinct you bring from a static language.

In C++ or Java, you write types everywhere because the compiler demands them. Arriving in Julia, you see ::Float64 in the syntax and reach for the same reflex — annotating every argument, pinning everything down. It feels rigorous. It’s actually counterproductive.

net_pay(gross, tax_rate) with no annotations is not a slower or sloppier function than net_pay(gross::Float64, tax_rate::Float64). It’s the same compiled code for Float64 inputs, plus a version for Int, plus one for Rational if someone needs exact arithmetic, plus one for a dual number if someone wants to automatically differentiate through it. You wrote it once. The compiler wrote the rest.

So the instinct to build is: annotate when you need to dispatch, when you need to store, or when you need to constrain the interface. Otherwise don’t. Generic code isn’t a stylistic preference in Julia; it’s how you get both reuse and speed from the same source.

2. Design around functions, not objects#

Coming from Python or Java, the instinct when modeling a domain is to make a class, give it fields, give it methods, and inherit. In Julia, split that instinct in two:

  • Structs hold data. They’re records with types. That’s all.

  • Functions carry behaviour, and dispatch decides which one runs.

Ask “what operations does this participate in?” rather than “what is this, and what does it inherit?” Then define those operations as methods on generic functions.

The payoff is extensibility in both directions. Someone can add a new type that works with all your existing functions. Someone else can add a new function that works with all your existing types. In a class hierarchy, only the first of those is easy — adding a new operation across an existing hierarchy means editing every class. Julia’s model makes both directions cheap, and that asymmetry is why Julia packages compose the way they do.

3. Let the compiler predict everything#

This is the deepest technical idea in the book, and once you see it you can’t unsee it. Ask of every value: can the compiler know what type this is?

  • Accumulator in a loop? → initialize it with the right type, not a bare 0.

  • Container? → give it a concrete element type.

  • Struct field that varies? → make the struct parametric, not abstract.

  • Working at the top level of a script? → put it in a function.

Every place the compiler can predict a type is a place it emits one instruction instead of a runtime method lookup and a heap allocation. Slow languages decide things over and over, on every call; Julia decides once, per type combination, and bakes in the answer.

And notice — from the last chapter — that this same discipline is what makes code trimmable into a small deployable binary. Type stability isn’t just a performance trick. It’s the property that lets the compiler commit.

4. Prototype and harden in the same file#

This is the workflow Julia was built for, and it’s worth being intentional about, because it’s the thing that brought me here.

In the two-language world, “make it work” and “make it fast” happen in different languages, by different people, at different times, with a translation step in between that quietly changes your results. In Julia they’re the same file, edited by the same person, on the same afternoon:

  • Explore freely. Work in the REPL or Pluto. Don’t annotate anything. Get the idea right.

  • Find the hot path. Profile. Almost always a small fraction of your code does the heavy lifting.

  • Harden what matters. Run @code_warntype on that fraction. Fix the instability. Preallocate. Add @views. Benchmark.

  • Scale it. Add @threads, or move an array to a CuArray.

Nothing was rewritten. The function that was fast on your laptop is the function that runs on the cluster. That bridge — the one I kept failing to cross during the pandemic with separate research and production stacks — is the entire reason this language exists.

5. Trust the ecosystem to compose, and write code that lets it#

Because generic functions are extensible by anyone, Julia packages combine in ways their authors never planned. A solver written against AbstractArray runs on a GPU array. An optimizer written against + and * runs on numbers that carry units, or uncertainty, or derivatives.

That composability isn’t magic — it’s a property of how the code was written, and you can write for it or against it. Write for it:

  • Accept AbstractVector, not Vector{Float64}.

  • Use similar, zero, one, and eltype instead of hardcoding 0.0 and Float64.

  • Define the small interface functions (iterate, getindex, size, isless, +) and get every generic algorithm in Base for free.

  • Don’t over-constrain your signatures.

Every unnecessary concrete type in a signature is a door you closed on a use case you didn’t imagine.

6. Read the source, always#

Julia’s standard library is written in Julia. Most packages are written in Julia. And @edit sort([1,2,3]) drops you straight into the implementation, in your editor.

This is a bigger deal than it sounds. In most languages, “how does this work?” bottoms out in C you can’t read or a runtime you can’t see. In Julia, curiosity is always satisfiable. When a function surprises you, read it. When you want to know how a package achieves something, @which the call and @edit the method.

I’ve learned more Julia from reading Base than from any documentation, and the code is unusually readable because it was written by people who expected it to be read.

7. Measure, don’t guess#

Julia makes performance so visible — @time reports allocations by default, @code_warntype colours the problems red, @btime gives you reliable numbers — that there’s genuinely no excuse for guessing.

And you should assume your guesses are wrong. Mine routinely are. I have optimized functions that turned out to be 2% of the runtime, and I have been certain a loop was the bottleneck when it was the logging call inside it.

The corollary: don’t optimize prematurely, but don’t be scared of optimizing either. In many languages, making code fast means making it worse — unrolling, inlining by hand, replacing clear code with clever code. In Julia, as we saw with the z-score example, optimization usually means being more precise about what you actually meant. The fast version is often the clearer one.

8. Start high, descend only when needed#

Julia spans an enormous range, from friendly array notation to @ccall and raw pointers. You don’t have to live at the bottom to benefit from it.

Write at the highest level that solves your problem. Use broadcasting, comprehensions, Vector, Dict, plain functions. Descend toward @inbounds, @simd, manual memory management, and C calls only when profiling shows you need to, and only in the small places that matter.

Premature low-level optimization makes code harder to read and rarely helps. The beauty of Julia is that the low level is there when you need it, not that you must use it everywhere.

Putting it all together#

Thinking in Julia, in one breath: write generic, expressive code and let the compiler specialize it; organize behaviour into functions and let dispatch choose; make every type predictable so the compiler can commit; prototype and harden in the same file; write against abstractions so other people’s code composes with yours; read the source when you’re curious; measure instead of guessing; and drop to the low level only where it truly pays.

Do that, and Julia stops feeling like “Python that makes you type end” and starts feeling like what it actually is: a language where the code you write to explore an idea is the same code that runs in production, on every core, on the GPU, without a rewrite and without a translator in the middle.

That’s the whole promise, and now you know how to think your way into it.

In the final chapter, we’ll take a tour of the standard library — the batteries that come included.