Writing your code

In this post, you will learn about tools to create, run and debug Julia code.

Getting help

TLDR: You're not alone!

Before you write any line of code, it's good to know where to find help. The official help page is a good place to start. In particular, the Julia community is always happy to guide beginners.

As a rule of thumb, the Discourse forum is where you should ask your questions to make the answers discoverable for future users. If you just want to chat with someone, you have a choice between the open source Zulip and the closed source Slack. Some of the vocabulary used by community members may appear unfamiliar, but don't worry: StartHere.jl gives you a good overview.

Installation

TLDR: Use juliaup

The most natural starting point to install Julia onto your system is the Julia downloads page, which will tell you to use juliaup.

  1. Windows users can download Julia and juliaup together from the Windows Store.
  2. OSX or Linux users can execute the following terminal command:
curl -fsSL https://install.julialang.org | sh

In both cases, this will make the juliaup and julia commands accessible from the terminal (or Windows Powershell). On Windows this will also create an application launcher. All users can start Julia by running

julia

Meanwhile, juliaup provides various utilities to download, update, organize and switch between different Julia versions. As a bonus, you no longer have to manually specify the path to your executable. This all works thanks to adaptive shortcuts called "channels", which allow you to access specific Julia versions without giving their exact number.

For instance, the release channel will always point to the current stable version, and the lts channel will always point to the long-term support version. Upon installation of juliaup, the current stable version of Julia is downloaded and selected as the default.

Advanced: To use other channels, add them to juliaup and put a + in front of the channel name when you start Julia:

 juliaup add lts
 julia +lts
 

You can get an overview of the channels installed on your computer with

 juliaup status
 

When new versions are tagged, the version associated with a given channel can change, which means a new executable needs to be downloaded. If you want to catch up with the latest developments, just do

 juliaup update
 

REPL

TLDR: The Julia REPL has 4 modes: Julia, package (]), help (?) and shell (;).

The Read-Eval-Print Loop (or REPL) is the most basic way to interact with Julia, check out its documentation for details. You can start a REPL by typing julia into a terminal, or by clicking on the Julia application in your computer. It will allow you to play around with arbitrary Julia code:

julia> a, b = 1, 2;

julia> a + b
3

This is the standard (Julia) mode of the REPL, but there are three other modes you need to know. Each mode is entered by typing a specific character after the julia> prompt. Once you're in a non-Julia mode, you stay there for every command you run. To exit it, hit backspace after the prompt and you'll get the julia> prompt back.

Help mode (?)

By pressing ? you can obtain information and metadata about Julia objects (functions, types, etc.) or unicode symbols. The query fetches the docstring of the object, which explains how to use it.

help?> println
println([io::IO], xs...)

Print (using ) xs to io followed by a newline. If io is not supplied, prints to the default output stream .

See also to add colors etc.

Examples

julia> println("Hello, world")
Hello, world

julia> io = IOBuffer();

julia> println(io, "Hello", ',', " world.")

julia> String(take!(io))
"Hello, world.\n"

If you don't know the exact name you are looking for, type a word surrounded by quotes to see in which docstrings it pops up.

Package mode (])

By pressing ] you access Pkg.jl, Julia's integrated package manager, whose documentation is an absolute must-read. Pkg.jl allows you to:

As an illustration, we download the package Example.jl inside our current environment:

(writing) pkg> add Example
┌ Warning: The Pkg REPL mode is intended for interactive use only, and should not be used from scripts. It is recommended to use the functional API instead.
@ Pkg.REPLMode /opt/hostedtoolcache/julia/1.10.5/x64/share/julia/stdlib/v1.10/Pkg/src/REPLMode/REPLMode.jl:382
   Resolving package versions...
   Installed Example ─ v0.5.3
    Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Project.toml`
  [7876af07] + Example v0.5.3
    Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Manifest.toml`
  [7876af07] + Example v0.5.3
Precompiling project...
Example
  1 dependency successfully precompiled in 0 seconds
(writing) pkg> status
Status `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Project.toml`
  [7876af07] Example v0.5.3

Note that the same keywords are also available in Julia mode:

julia> using Pkg

julia> Pkg.rm("Example")
    Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Project.toml`
  [7876af07] - Example v0.5.3
    Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Manifest.toml`
  [7876af07] - Example v0.5.3

The package mode itself also has a help mode, accessed with ?, in case you're lost among all these new keywords.

Shell mode (;)

By pressing ; you enter a terminal, where you can execute any command you want. Here's an example for Unix systems:

shell> ls ./writing
Manifest.toml
Project.toml
index.md

Editor

TLDR: VSCode is the IDE with the best Julia support.

Most computer programs are just plain text files with a specific extension (in our case .jl). So in theory, any text editor suffices to write and modify Julia code. In practice, an Integrated Development Environment (or IDE) makes the experience much more pleasant, thanks to code-related utilities and language-specific plugins.

The best IDE for Julia is Visual Studio Code, or VSCode, developed by Microsoft. Indeed, the Julia VSCode extension is the most feature-rich of all Julia IDE plugins. You can download it from the VSCode Marketplace and read its documentation.

VSCode: In what follows, we will sometimes mention commands and keyboard shortcuts provided by this extension. But the only shortcut you need to remember is Ctrl + Shift + P (or Cmd + Shift + P on Mac): this opens the VSCode command palette, in which you can search for any command. Type "julia" in the command palette to see what you can do.

Advanced: Assuming you want to avoid the Microsoft ecosystem, VSCodium is a nearly bit-for-bit replacement for VSCode, but with an open source license and without telemetry. If you don't want to use VSCode at all, other options include Emacs and Vim. Check out JuliaEditorSupport to see if your favorite IDE has a Julia plugin. The available functionalities should be roughly similar to those of VSCode, at least for the basic aspects like running code.

You may also want to download the JuliaMono font for esthetically pleasant unicode handling.

Running code

TLDR: Open a REPL and run all your code there interactively.

You can execute a Julia script from your terminal, but in most cases that is not what you want to do.

julia myfile.jl  # avoid this

Julia has a rather high startup and compilation latency. If you only use scripts, you will pay this cost every time you run a slightly modified version of your code. That is why many Julia developers fire up a REPL at the beginning of the day and run all of their code there, chunk by chunk, in an interactive way. Full files can be run interactively from the REPL with the include function.

julia> include("myfile.jl")

Alternatively, includet from the Revise.jl package can be used to "include and track" a file. This will automatically update changes to function definitions in the file in the running REPL session.

VSCode: Running code is made much easier by the following commands:

  • Julia: Restart REPL (shortcut Alt + J then Alt + R) - this will open or restart the integrated Julia REPL. It is different from opening a plain VSCode terminal and launching Julia manually from there.
  • Julia: Execute Code in REPL and Move (shortcut Shift + Enter) - this will execute the selected code in the integrated Julia REPL, like a notebook.

When keeping the same REPL open for a long time, it's common to end up with a "polluted" workspace where the definitions of certain variables or functions have been overwritten in unexpected ways. This, along with other events like struct redefinitions, might force you to restart your REPL now and again, and that's okay.

Notebooks

TLDR: Try either Jupyter or Pluto, depending on your reactivity needs.

Notebooks are a popular alternative to IDEs when it comes to short and self-contained code, typically in data science. They are also a good fit for literate programming, where lines of code are interspersed with comments and explanations.

The most well-known notebook ecosystem is Jupyter, which supports Julia, Python and R as its three core languages. To use it with Julia, you will need to install the IJulia.jl backend. Then, if you have also installed Jupyter with pip install jupyterlab, you can run this command to launch the server:

jupyter lab

If you only have IJulia.jl on your system, you can run this snippet instead:

julia> using IJulia

julia> IJulia.notebook()

VSCode: Jupyter notebooks can be opened, modified and run directly from the editor. Thanks to the Julia extension, you don't even need to install IJulia.jl or Jupyter first.

A pure-Julia alternative to Jupyter is given by Pluto.jl. Unlike Jupyter notebooks, Pluto notebooks are

To try them out, install the package and then run

julia> using Pluto

julia> Pluto.run()

Markdown

TLDR: Markdown is also a good fit for literate programming, and Quarto is an alternative to notebooks.

Markdown is a markup language used to add formatting elements to plaintext text files.

Plain Text Markdown

Plain text markdown files, which have the .md extension, are not used for interactive programming, meaning one cannot run code written in the file. As a result, plain text markdown files are usually rendered into a final product by other software.

This is an example of a plain text markdown file:

# Title

## Section Header

This is example text.

```julia
println("hello world")
```

Quarto

Quarto "is an open-source scientific and technical publishing system." Quarto makes a plain text markdown file (.md) alternative called Quarto markdown file (.qmd).

Quarto markdown files like plain text markdown files also integrate with editors, such as VSCode.

VSCode: Install the Quarto extension for a streamlined experience.

Unlike plain text markdown files, Quarto markdown files have executable code chunks. These code chunks provide a functionality similar to notebooks, thus Quarto markdown files are an alternative to notebooks. Additionally, Quarto markdown files give users additional control over output and styling via the YAML header at the top of the .qmd file.

As of Quarto version 1.5, users can choose from two Julia engines to execute code - a native Julia engine and IJulia.jl. The primary difference between the native Julia engine and IJulia.jl is that the native Julia engine does not depend on Python and can utilize local environments. For this reason it's recommended to start with the native Julia engine. Learn more about the native Julia engine in Quarto's documentation.

Below is an example of a Quarto markdown file.

---
title: "My document"
format:
  # renders a HTML document
  html:
    # table of contents
    toc: true
execute:
  # makes code chunks invisible in the output
  # code output is still visible though
  echo: false
  # hides warnings in the output
  warning: false
# native julia engine
engine: julia
---

# Title

## Section Header

Below is an executable code chunk.

If this file were opened in an editor such as VSCode one could execute the `println("hello world")` Julia code and view the output, like in a notebook.

```{julia}
println("hello world")
```

Environments

TLDR: Activate a local environment for each project with ]activate path. Its details are stored in Project.toml and Manifest.toml.

As we have seen, Pkg.jl is the Julia equivalent of pip or conda for Python. It lets you install packages and manage environments (collections of packages with specific versions).

You can activate an environment from the Pkg REPL by specifying its path ]activate somepath. Typically, you would do ]activate . to activate the environment in the current directory. Another option is to directly start Julia inside an environment, with the command line option julia --project=somepath.

Once in an environment, the packages you ]add will be listed in two files somepath/Project.toml and somepath/Manifest.toml:

If you haven't entered any local project, packages will be installed in the default environment, called @v1.X after the active version of Julia (note the @ before the name). Packages installed that way are available no matter which local environment is active, because of "environment stacking". It is therefore recommended to keep the default environment very light, containing only essential development tools.

VSCode: You can configure the environment in which a VSCode Julia REPL opens. Just click the Julia env: ... button at the bottom. Note however that the Julia version itself will always be the default one from juliaup.

Advanced: You can visualize the dependency graph of an environment with PkgDependency.jl.

Local packages

TLDR: A package makes your code modular and reproducible.

Once your code base grows beyond a few scripts, you will want to create a package of your own. The first advantage is that you don't need to specify the path of every file: using MyPackage: myfunc is enough to get access to the names you define. Furthermore, you can specify versions for your package and its dependencies, making your code easier and safer to reuse.

To create a new package locally, the easy way is to use ]generate (we will discuss a more sophisticated workflow in the next blog post).

julia> Pkg.generate(sitepath("MyPackage"));  # ignore sitepath
  Generating  project MyPackage:
    ~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/__site/MyPackage/Project.toml
    ~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/__site/MyPackage/src/MyPackage.jl

This command initializes a simple folder with a Project.toml and a src subfolder. As we have seen, the Project.toml specifies the dependencies. Meanwhile, the src subfolder contains a file MyPackage.jl, where a module called MyPackage is defined. It is the heart of your package, and will typically look like this when you're done:

module MyPackage

# imported dependencies
using OtherPackage1
using OtherPackage2

# files defining functions, types, etc.
include("file1.jl")
include("subfolder/file2.jl")

# names you want to make public
export myfunc
export MyType

end

Development workflow

TLDR: Use Revise.jl to track code changes while you play with your package in its own environment.

Once you have created a package, your daily routine will look like this:

  1. Open a REPL in which you import MyPackage
  2. Run some functions interactively, either by writing them directly in the REPL or from a Julia file that you use as a notebook
  3. Modify some files in MyPackage
  4. Go back to step 2

For that to work well, you need code modifications to be taken into account automatically. That is why Revise.jl exists. In fact, it is used by so many Julia developers that some wish it were part of the core language: you can read its documentation for more details. If you start every REPL session by importing Revise.jl, then all the other packages you import after that will have their code tracked. Whenever you edit a source file and hit save, the REPL will update its state accordingly.

VSCode: The Julia extension imports Revise.jl by default when it starts a REPL.

The only remaining question is: in which environment should you work? In general, you can work within the environment defined by your package, and add all the dependencies you need there. To summarize, this is how you get started:

using Revise, Pkg
Pkg.activate("./MyPackage")
using MyPackage
MyPackage.myfunc()

Advanced: There are situations where the previous method does not work:

  • if you are developing several packages at once and want to use them together
  • if your interactive work requires heavy dependencies that your package itself does not need (for instance plotting).

Then, you will need to use another environment as a playground, and ]develop (or ]dev) your package(s) into it. Note the new Pkg.jl keyword: ]add PackageName is used to download a fixed version of a registered package, while ]develop path links to the current state of the code in a local folder. To summarize, this is how you get started:

 using Revise, Pkg
 Pkg.activate("./MyPlayground")
 Pkg.develop(path="./MyPackage")
 using MyPackage
 MyPackage.myfunc()
 

Configuration

TLDR: Use the startup file to import packages as soon as Julia starts.

Julia accepts startup flags to handle settings such as the number of threads available or the environment in which it launches. In addition, most Julia developers also have a startup file which is run automatically every time the language is started. It is located at .julia/config/startup.jl.

The basic component that everyone puts in the startup file is Revise.jl:

try
    using Revise
catch e
    @warn "Error initializing Revise"
end

In addition, users commonly import packages that affect the REPL experience, as well as esthetic, benchmarking or profiling utilities. A typical example is OhMyREPL.jl which is widely used for syntax highlighting in the REPL. More generally, the startup file allows you to define your own favorite helper functions and have them immediately available in every Julia session. StartupCustomizer.jl can help you set up your startup file.

Advanced: Here are a few more startup packages that can make your life easier once you know the language better:

Interactivity

TLDR: Explore source code from within the REPL.

The Julia REPL comes bundled with InteractiveUtils.jl, a bunch of very useful functions for interacting with source code.

Here are a few examples:

julia> supertypes(Int64)
(Int64, Signed, Integer, Real, Number, Any)

julia> subtypes(Integer)
Any[Bool, Signed, Unsigned]

julia> length(methodswith(Integer))
707

julia> @which exp(1)
exp(x::Real) @ Base.Math math.jl:1575

julia> apropos("matrix exponential")
Base.:^
Base.exp

When you ask for help on a Julia forum, you might want to include your local Julia information:

julia> versioninfo()
Julia Version 1.10.5
Commit 6f3fdf7b362 (2024-08-27 14:19 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × AMD EPYC 7763 64-Core Processor
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-15.0.7 (ORCJIT, znver3)
Threads: 1 default, 0 interactive, 1 GC (on 4 virtual cores)
Environment:
  JULIA_PRE = 
  JULIA_POST = 
  JULIA_DEBUG =

Advanced: The following packages can give you even more interactive power:

Logging

TLDR: Logging macros are more versatile than printing.

When you encounter a problem in your code or want to track progress, a common reflex is to add print statements everywhere.

function printing_func(n)
    for i in 1:n
        println(i^2)
    end
end
julia> printing_func(3)
1
4
9

A slight improvement is given by the @show macro, which displays the variable name:

function showing_func(n)
    for i in 1:n
        @show i^2
    end
end
julia> showing_func(3)
i ^ 2 = 1
i ^ 2 = 4
i ^ 2 = 9

But you can go even further with the macros @debug, @info, @warn and @error. They have several advantages over printing:

function warning_func(n)
    for i in 1:n
        @warn "This is bad" i^2
    end
end
julia> warning_func(3)
┌ Warning: This is bad
  i ^ 2 = 1
@ [Franklin]:3
┌ Warning: This is bad
  i ^ 2 = 4
@ [Franklin]:3
┌ Warning: This is bad
  i ^ 2 = 9
@ [Franklin]:3

Refer to the logging documentation for more information.

Advanced: In particular, note that @debug messages are suppressed by default. You can enable them through the JULIA_DEBUG environment variable if you specify the source module name, typically Main or your package module.

Beyond the built-in logging utilities, ProgressLogging.jl has a macro @progress, which interfaces nicely with VSCode and Pluto to display progress bars. And Suppressor.jl can sometimes be handy when you need to suppress warnings or other bothersome messages (use at your own risk).

Debugging

TLDR: Infiltrator.jl and Debugger.jl allow you to peek inside a function while its execution is paused.

The problem with printing or logging is that you cannot interact with local variables or save them for further analysis. The following two packages solve this issue, and they probably belong in your default environment @v1.X, like Revise.jl.

Setting

Assume you want to debug a function checking whether the \(n\)-th Fermat number \(F_n = 2^{2^n} + 1\) is prime:

function fermat_prime(n)
    k = 2^n
    F = 2^k + 1
    for d in 2:isqrt(F)  # integer square root
        if F % d == 0
            return false
        end
    end
    return true
end
julia> fermat_prime(4)
true

julia> fermat_prime(6)
true

Unfortunately, \(F_4 = 65537\) is the largest known Fermat prime, which means \(F_6\) is incorrectly classified. Let's investigate why this happens!

Infiltrator.jl

Infiltrator.jl is a lightweight inspection package, which will not slow down your code at all. Its @infiltrate macro allows you to directly set breakpoints in your code. Calling a function which hits a breakpoint will activate the Infiltrator REPL-mode and change the prompt to infil>. Typing ? in this mode will summarize available commands. For example, typing @locals in Infiltrator-mode will print local variables:

using Infiltrator

function fermat_prime_infil(n)
    k = 2^n
    F = 2^k + 1
    @infiltrate
    for d in 2:isqrt(F)
        if F % d == 0
            return false
        end
    end
    return true
end

What makes Infiltrator.jl even more powerful is the @exfiltrate macro, which allows you to move local variables into a global storage called the safehouse.

julia> fermat_prime_infil(6)
Infiltrating fermat_prime_infil(n::Int64)
  at REPL[2]:4

infil> @exfiltrate k F
Exfiltrating 2 local variables into the safehouse.

infil> @continue

true

julia> safehouse.k
64

julia> safehouse.F
1

The diagnosis is a classic one: integer overflow. Indeed, \(2^{64}\) is larger than the maximum integer value in Julia:

julia> typemax(Int)
9223372036854775807

julia> 2^63-1
9223372036854775807

And the solution is to call our function on "big" integers with an arbitrary number of bits:

julia> fermat_prime(big(6))
false

Debugger.jl

Debugger.jl allows us to interrupt code execution anywhere we want, even in functions we did not write. Using its @enter macro, we can enter a function call and walk through the call stack, at the cost of reduced performance.

The REPL prompt changes to 1|debug>, allowing you to use custom navigation commands to step into and out of function calls, show local variables and set breakpoints. Typing a backtick ` will change the prompt to 1|julia>, indicating evaluation mode. Any expression typed in this mode will be evaluated in the local context. This is useful to show local variables, as demonstrated in the following example:

julia> using Debugger

julia> @enter fermat_prime(6)
In fermat_prime(n) at REPL[7]:1
 1  function fermat_prime(n)
>2      k = 2^n
 3      F = 2^k + 1
 4      for d in 2:isqrt(F)  # integer square root
 5          if F % d == 0
 6              return false

About to run: (^)(2, 6)
1|debug> n
In fermat_prime(n) at REPL[7]:1
 1  function fermat_prime(n)
 2      k = 2^n
>3      F = 2^k + 1
 4      for d in 2:isqrt(F)  # integer square root
 5          if F % d == 0
 6              return false
 7          end

About to run: (^)(2, 64)
1|julia> k
64

VSCode: VSCode offers a nice graphical interface for debugging. Click left of a line number in an editor pane to add a breakpoint, which is represented by a red circle. In the debugging pane of the Julia extension, click Run and Debug to start the debugger. The program will automatically halt when it hits a breakpoint. Using the toolbar at the top of the editor, you can then continue, step over, step into and step out of your code. The debugger will open a pane showing information about the code such as local variables inside of the current function, their current values and the full call stack.

The debugger can be sped up by selectively compiling modules that you will not need to step into via the + symbol at the bottom of the debugging pane. It is often easiest to start by adding ALL_MODULES_EXCEPT_MAIN to the compiled list, and then selectively remove the modules you need to have interpreted.

CC BY-SA 4.0 G. Dalle, J. Smit, A. Hill. Last modified: September 12, 2024.
Website built with Franklin.jl and the Julia programming language.