Julia Workflow Tips and Tricks

For a Better Developer Experience

Author

Dheepak Krishnamurthy

Published

August 13, 2023

Keywords

julia, tips, tricks, developer, experience, workflow

Julia is a high-performance, dynamic programming language designed for technical computing and data science. The language offers a flexible and expressive syntax, and also delivers the performance benefits of compiled languages like C++. Like Python, Julia also offers an interactive REPL (Read-Eval-Print Loop) environment for real-time data exploration and rapid prototyping.

If you are fairly new to Julia, here are a few things you might want to know to improve your developer experience.

startup.jl

Every time you start Julia, it looks for a file named startup.jl in a config directory.

Operating System startup.jl Location
Windows C:\Users\USERNAME\.julia\config\startup.jl
macOS /Users/USERNAME/.julia/config/startup.jl
Linux /home/USERNAME/.julia/config/startup.jl

This file is executed before the REPL starts, allowing you to customize your Julia environment.

For example, if you added the following code to your startup.jl, it would ensure that Revise, OhMyREPL and BenchmarkTools are always installed whenever you start Julia.

# Setup OhMyREPL and Revise
import Pkg
let
  pkgs = ["Revise", "OhMyREPL", "BenchmarkTools"]
  for pkg in pkgs
    if Base.find_package(pkg) === nothing
      Pkg.add(pkg)
    end
  end
end

When adding packages in Julia, they are added by default to what’s known as the global environment. While these packages can be readily used within the Julia REPL, they won’t be available for import within Julia packages.

For those aiming to ensure reproducibility in code execution — both for themselves and others — it’s essential to use dedicated, per-project local environments. This approach ensures that the same set of package versions are always used.

Personally, I reserve the global environment for packages that enhance my development workflow, such as Revise, BenchmarkTools, and OhMyREPL.

This thread on the JuliaLang Discourse has some neat examples of things you may want to add to your startup.jl.

If you wish to go one step further, you can create a Julia package called Startup.jl, place it anywhere on your computer and load it in startup.jl using the following code:

if Base.isinteractive()
  push!(LOAD_PATH, joinpath(ENV["HOME"], "gitrepos", "Startup.jl"))
  using Startup
end

The advantage of this approach is that the contents of Startup.jl can be precompiled and your Julia REPL starts up very quickly.

With the Startup.jl package approach, your root environment can be empty and you can still use functionality exposed by Startup.jl:

Using Startup.jl to load BenchmarkTools with an empty root environment.

Using Startup.jl to load BenchmarkTools with an empty root environment.

You can use julia --startup-file=no to not execute the contents for startup.jl, i.e. get a clean Julia REPL.

OhMyREPL

OhMyREPL.jl is a package that provides syntax highlighting, bracket highlighting, rainbow brackets, and more for the Julia REPL. Once installed, it can enhance your REPL experience dramatically, making it more visually appealing and easier to work with.

Revise.jl

Revise.jl is a game-changer for Julia development. It automatically reloads modified source files without restarting the Julia session. This makes iterative development much smoother.

Once set up, any changes you make to your code files are immediately available in your active Julia session.

Revise can track changes in single files if you include them using includet:

If you want Revise to track changes in a package you are developing locally, simply run using Revise before you load the package for the first time.

If you use VSCode and start a REPL using the Julia: Start REPL command, Revise is automatically loaded by default.

See Revise’s documentation for how to set this up to happen automatically all the time by adding setup code to your startup.jl file.

One of the issues with Revise is that it cannot deal with changes in struct. Let’s say I wanted to make a change like this:

  struct Foo
-   x::Int
+   y::Float64
  end

Revise throws an error and warning because it is unable to make that change; and Revise changes the color of the Julia prompt:

There are some workarounds for this but the easiest thing to do is to restart the Julia session after you are make changes to any struct.

Watch for the Revise warnings and errors and keep an eye out for the color of your Julia REPL prompt. If you see the prompt in yellow, you know Revise wasn’t able to track a change you made.

Infiltrator

Infiltrator.jl is a debugger and code inspection tool for Julia. It allows you to insert breakpoints in your code and inspect the current scope’s variables.

When Julia hits the @infiltrate macro, it’ll pause execution, and drop you into a Julia REPL that allows you to inspect the current scope.

You can also use @infiltrate i == 3 and that’ll drop you into a Julia REPL only in the third iteration of the for loop.

When using @infiltrate conditional_expression with Revise, you can jump into any function at any point of the execution to inspect values of variables in a Julia REPL. You can even load additional packages like DataFrames or Plots to explore your data in the scope of any function interactively while debugging. This combination can make for a versatile and productive debugging experience.

PrecompileTools

PrecompileTools is a package that allows package developers to specify which parts of the package should be precompiled. From the official documentation:

PrecompileTools can force precompilation of specific workloads; particularly with Julia 1.9 and higher, the precompiled code can be saved to disk, so that it doesn’t need to be compiled freshly in each Julia session. You can use PrecompileTools as a package developer, to reduce the latency experienced by users of your package for typical workloads; you can also use PrecompileTools as a user, creating custom Startup package(s) that precompile workloads important for your work.

Precompiling Julia packages can significantly reduce the loading times for you and your users, providing a much more responsive experience.

For example, I have the following in my Startup.jl to reduce Julia REPL startup times:

module Startup

using PrecompileTools

@setup_workload begin
  @compile_workload begin
    using Pkg
    using Revise
    using OhMyREPL
  end
end

end

BenchmarkTools

BenchmarkTools.jl is an essential package to help quantify the performance of your code. It provides utilities to benchmark code snippets, giving insights into their run-time and memory allocations.

The @benchmark macro runs a number of trials of a function and plots a histogram in the terminal showing what kind of performance you are getting out of that particular function. As an example, we can compare the performance of a custom sum function without and with the @simd macro:

Cthulhu

Delving deep into Julia’s compiler optimizations and type inference can sometimes feel daunting and that’s where Cthulhu.jl comes to the rescue. Cthulhu is an interactive terminal user interface that is an alternative to @code_lowered and @code_typed, and allows developers to interactively descend into the lowered and optimized versions of their Julia code, making it easier to debug performance issues and understand how Julia’s JIT compiler optimizes code.

For example, if we examine the mysum function from the previous section, we can see that a is being inferred as a Int64 or a Float64, i.e. Union{Float64, Int64}.

In this particular case, by changing a = 0 to a = 0.0, we can make the code generated by Julia more optimized, i.e. a is now being inferred only as a Float64.

Here’s the benchmark results with a = 0 (left) and a = 0.0 (right), with the latter being almost 3 times faster.

`a = 0`

`a = 0.0`

ReTest and InlineTests

Julia has a built-in package Test for unit testing. This requires writing tests in a separate folder, i.e. in test/runtests.jl; and these tests are run in a separate process when you can Pkg.test(). There’s also no out of the box solution to run a subset of tests.

Using InlineTests allows you to write tests directly in your source files, and you can also choose to run a subset of tests. If you choose to run it with retest, you can make changes that are tracked with Revise, allowing faster iteration using a test driven development workflow.

In the screenshot below, I have shown an example of having a @testset as part of the package itself, i.e. in ./src/layout.jl:

InlineTests allows co-locating tests along with the implementation.

InlineTests allows co-locating tests along with the implementation.

You’ll also notice from the screenshot that ReTest contains a function called retest which allows running a subset of tests by passing in a pattern as the second argument.

You may also be interested in TestEnv.jl, which lets you activate the test environment of a given package.

PkgTemplates

Julia has a built-in way to create a new package, using Pkg.generate(). However, PkgTemplates.jl is a package that makes the process of creating new packages as easy and customizable at the same time. PkgTemplates can generate new Julia packages with numerous features out-of-the-box that follow best practices (e.g. GitHub Actions, Documenter, etc).

Creating a new package then becomes as straightforward as running the following:

using PkgTemplates
t = Template()
t("MyNewPackage")

You can customize the template too:

using PkgTemplates
t = Template(
  dir = "~/gitrepos/",
  julia = v"1.10",
  plugins = [
    Git(; ssh = true, manifest = true),
    GitHubActions(),
    Documenter{GitHubActions}(),
  ],
)
t("MyNewPackage")

JET

JET.jl is a powerful static code analyzer tailored for the Julia programming language. JET serves as a linter – identifying potential runtime errors without actually executing the code. It works by simulating the Julia compiler’s type inference process.

fib(n) = n  2 ? n : fib(n-1) + fib(n-2)

fib(1000)   # => never terminates
fib(m)      # => ERROR: UndefVarError: `m` not defined
fib("1000") # => ERROR: MethodError: no method matching isless(::String, ::Int64)

You can see the other kinds of warnings that JET can produce in the demo.jl file.

LiveServer

LiveServer.jl allows developers to serve static sites locally and refreshes your browser automatically as you edit source files. You can even use it to view or update documentation.

# serve contents of any folder
LiveServer.serve(; dir, launch_browser = true)

# serve documentation of any package
Pkg.activate("./docs")
LiveServer.servedocs(; launch_browser = true)

Comonicon

Comonicon.jl is a rich tool for building command-line applications. Comonicon allows you to easily convert your Julia scripts and packages into command-line tools with minimal effort.

Using Comonicon, you can:

  • Parse command-line arguments
  • Generate help messages
  • Handle subcommands
  • And more

For example, I’ve added into my Startup.jl/src/jl.jl some helper subcommands that I can access from that command line in any folder.

Conclusion

Julia’s ecosystem is filled with tools designed to optimize the developer’s workflow, making it easier and more efficient to write, test, and deploy code.

Reuse

Citation

BibTeX citation:
@online{krishnamurthy2023,
  author = {Krishnamurthy, Dheepak},
  title = {Julia {Workflow} {Tips} and {Tricks}},
  date = {2023-08-13},
  url = {https://kdheepak.com/blog/julia-workflow-tips-and-tricks/},
  langid = {en}
}
For attribution, please cite this work as:
D. Krishnamurthy, “Julia Workflow Tips and Tricks,” Aug. 13, 2023. https://kdheepak.com/blog/julia-workflow-tips-and-tricks/.