Skip to main content

Command Palette

Search for a command to run...

Discover the Key Features and Updates in Julia 1.12

Explore the latest updates in Julia 1.12.

Updated
7 min read
Discover the Key Features and Updates in Julia 1.12
G

Modeling & Simulation, HPC, and Enterprise Software all under one roof.

Great Lakes Consulting Services, Inc., a premier Information Technology Consulting company, serving others in IT staffing, analytic consulting, business intelligence and application development since 2009. We now specialize in custom Julia software services as the trusted partner to JuliaHub for their Consulting Services. Since 2015, we’ve partnered together to develop high-performance Julia code for low-latency data visualization and analytic solutions, high performance financial modeling, Modeling and Simulation for multiple sciences, personal Julia training, and legacy code migration & evolution.

This post was written by Steven Whitaker.

A new version of the Julia programming language was just released! Version 1.12 is now the latest stable version of Julia.

This release is a minor release, meaning it includes language enhancements and bug fixes but should also be fully compatible with code written in previous Julia versions (from version 1.0 and onward).

In this post, we will check out some of the features and improvements introduced in this newest Julia version. Read the full post, or click on the links below to jump to the features that interest you.

If you are new to Julia (or just need a refresher), feel free to check out our Julia tutorial series, beginning with how to install Julia and VS Code.

Improved Quality of Life with Redefinable Types

Julia 1.12 introduces a major quality of life improvement for package development by allowing types to be redefined. To illustrate, prior to Julia 1.12:

julia> struct A end

julia> struct A
           x::Int
       end
ERROR: invalid redefinition of constant Main.A
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

But in Julia 1.12, no error is thrown! Note, however, that any objects of the old type will still be of the old type---they don't automatically update to conform to the new type definition. For example:

julia> struct A end

julia> a1 = A()
A()

julia> struct A
           x::Int
       end

julia> a2 = A(1)
A(1)

julia> a1
@world(A, 38513:38515)()

Note that after redefining A, a1 is denoted as being of type A from a previous so-called "world age".

Importantly, when defining a method that dispatches on a type, it will be defined for the version of the type that existed at the time of method definition. Continuing from the previous example:

julia> f(a::A) = println("hello")
f (generic function with 1 method)

julia> f(a2)
hello

julia> f(a1) # Method doesn't exist for the old type
ERROR: MethodError: no method matching f(::@world(A, 38513:38515))
The function `f` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  f(::A)
   @ Main REPL[6]:1

Stacktrace:
 [1] top-level scope
   @ REPL[8]:1

julia> struct A # Update the type
           x::Float64
       end

julia> f(A(2.0)) # Method doesn't exist for the new type
ERROR: MethodError: no method matching f(::A)
The function `f` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  f(::@world(A, 38516:38521))
   @ Main REPL[6]:1

Stacktrace:
 [1] top-level scope
   @ REPL[18]:1

In other words, if updating a type, be sure to update methods as well.

Often, however, when someone is working with a type and many methods that use the type for dispatch, they are developing a package. Package development works differently than just working in the REPL, so let's see how package development is affected by the ability to update types.

First, without proper tooling, Julia package development can be a slog, regardless of whether types (or anything else) can be updated. This is because normally, after a package is loaded, changes made to the package's source code don't take effect until Julia is restarted and the package is loaded again. In other words, the whole Julia runtime has to be started up again and code has to be reloaded even if just a single function in the package was updated.

Fortunately, Revise.jl exists. Revise.jl provides massive quality of life improvements for package developers by allowing changes to the package source code to take effect immediately, without restarting Julia.

The biggest caveat? Revise.jl couldn't handle changes to struct definitions. So, if you decide a struct in your package needs an extra field, you're out of luck, you have to restart Julia. And then if you decide the struct actually didn't need that field, too bad again, restart Julia.

But that all changes with Julia 1.12! This release adds a mechanism for redefining types that Revise.jl hooks into, removing arguably the most significant remaining pain point of Julia package development. The number of times a developer needs to restart Julia will decrease significantly with Julia 1.12, allowing for greater productivity.

New Way to Fix Function Arguments

Julia 1.12 introduces the Fix struct that can be used to fix function arguments. (Here, "to fix" means "to set", not "to correct".) Think of it as another way to define a closure.

One of the main uses of Fix is purely for convenience, particularly when creating closures of functions with many inputs. For example, suppose we have the following (nonsensical) physical model:

function dynamics(velocity, resistance, gravity, position, friction, length, mass)
    return velocity + resistance + gravity + position + friction + length + mass
end

Now, suppose we have another function that computes (or accepts as input) a particular value for mass and then creates a new function that computes dynamics with mass fixed. Previously, an anonymous function typically would be used, so let's compare using an anonymous function to using Fix:

mass = 30 # Some computed or given value

# Using an anonymous function.
# Note that the `let mass = mass` is essential
# to guarantee Julia doesn't box `mass`
# (see <https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured>).
# (Though also note that sometimes boxing does not occur
# even if the `let` block is omitted.)
dynamics_with_mass = let mass = mass
    (velocity, resistance, gravity, position, friction, length) -> dynamics(velocity, resistance, gravity, position, friction, length, mass)
end

# Using `Fix`.
# The `7` indicates that we want to fix the 7th argument.
dynamics_with_mass_fix = Base.Fix{7}(dynamics, mass)

As you can see, using Fix is much more concise. But both dynamics_with_mass and dynamics_with_mass_fix act as a function of six arguments and compute dynamics with mass fixed to a specific value.

Next, suppose we want to compute the derivative of dynamics as a function of velocity for fixed values of the other parameters. In this case, there are six arguments that need to be fixed. Fix allows for fixing just one argument, but Fixes can be nested to fix multiple arguments. Let's compare using an anonymous function to using Fix:

# Fixed values, either computed or given.
resistance = 2
gravity = 9.8
position = 0
friction = 0
length = 0.5
mass = 30

# Using an anonymous function.
f = let resistance = resistance,
        gravity = gravity,
        position = position,
        friction = friction,
        length = length,
        mass = mass
    velocity -> dynamics(velocity, resistance, gravity, position, friction, length, mass)
end

# Using `Fix`.
f_fix = Base.Fix{2}(Base.Fix{2}(Base.Fix{2}(Base.Fix{2}(Base.Fix{2}(Base.Fix{2}(dynamics, resistance), gravity), position), friction), length), mass)

# Using `Fix` with piping for nicer formatting.
pipefix(::Val{N}, x) where {N} = Base.Fix{2}(Base.Fix{N}, x)
f_pipefix = Base.Fix{2}(dynamics, resistance) |>
    pipefix(Val{2}(), gravity) |>
    pipefix(Val{2}(), position) |>
    pipefix(Val{2}(), friction) |>
    pipefix(Val{2}(), length) |>
    pipefix(Val{2}(), mass)

In this case, I would argue using an anonymous function is clearer to read. However, Fix can have some performance advantages over anonymous functions. For example, using Fix can reduce the need to compile anonymous functions that are used repeatedly.

For a lot more context and discussion, see the pull request that introduced Fix.

Progress Towards More Reasonable Executables

Perhaps surprisingly, an experimental feature is probably the most highly anticipated addition in Julia 1.12: a new command-line argument --trim that enables a (currently experimental) feature that removes unnecessary code from compiled executables.

Along with the release of Julia 1.12, JuliaC.jl was also created to streamline the creation of executables. JuliaC.jl can be installed as a Julia app (another new feature with 1.12!):

pkg> app add JuliaC

Installing JuliaC.jl in this way creates the juliac executable at ~/.julia/bin/juliac. (After installation, you may want to add ~/.julia/bin to your PATH.)

Let's see how to use juliac with a simple program:

# main.jl

function @main(args::Vector{String})::Cint
    for arg in args
        # Note the use of `Core.stdout` instead of `stdout`
        # (which is used by default if an `io` argument is omitted).
        println(Core.stdout, arg)
    end
    return 0
end

This program simply prints to the console all its given command-line arguments.

To compile it into an executable, just run the following command:

juliac --output-exe print_args --project . --bundle bundle --trim main.jl

To verify it works:

$ ./bundle/bin/print_args hello world
hello
world

So, what exactly is the impact of --trim? Without the new (experimental) feature, the size of the print_args executable was 205 MB on my computer. With --trim, that number improved by over 100x, dropping to just 1.7 MB!

Summary

In this post, we learned about some of the new features and improvements introduced in Julia 1.12, including redefinable types, Fix, and --trim. Curious readers can check out the release notes for the full list of changes.

What are you most excited about in Julia 1.12? Let us know in the comments below!

Additional Links

571 views

Julia Releases

Part 1 of 3

GLCS discusses new Julia releases, including new features, improvements, and our recommendations. Stay up to date on the latest in Julia!

Up next

Julia 1.11: Top Features and Important Updates

Learn about the new features in Julia 1.11.