Re-posted from: https://blog.glcs.io/julia-1-12
This post was written by Steven Whitaker.
A new version of the Julia programming languagewas just released!Version 1.12 is now the latest stable version of Julia.
This release is a minor release,meaning it includes language enhancementsand bug fixesbut should also be fully compatiblewith code written in previous Julia versions(from version 1.0 and onward).
In this post,we will check out some of the features and improvementsintroduced in this newest Julia version.Read the full post,or click on the links belowto jump to the features that interest you.
- Improved Quality of Life with Redefinable Types
- New Way to Fix Function Arguments
- Progress Towards More Reasonable Executables
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 improvementfor package developmentby allowing types to be redefined.To illustrate,prior to Julia 1.12:
julia> struct A endjulia> struct A x::Int endERROR: invalid redefinition of constant Main.AStacktrace: [1] top-level scope @ REPL[2]:1
But in Julia 1.12,no error is thrown!Note, however,that any objects of the old typewill still be of the old type—they don’t automatically updateto conform to the new type definition.For example:
julia> struct A endjulia> a1 = A()A()julia> struct A x::Int endjulia> a2 = A(1)A(1)julia> a1@world(A, 38513:38515)()
Note that after redefining A,a1 is denoted as being of type Afrom 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 typethat 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)hellojulia> f(a1) # Method doesn't exist for the old typeERROR: 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]:1Stacktrace: [1] top-level scope @ REPL[8]:1julia> struct A # Update the type x::Float64 endjulia> f(A(2.0)) # Method doesn't exist for the new typeERROR: 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]:1Stacktrace: [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 typeand many methods that use the type for dispatch,they are developing a package.Package development works differentlythan just working in the REPL,so let’s see how package developmentis 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 codedon’t take effectuntil Julia is restartedand the package is loaded again.In other words,the whole Julia runtimehas to be started up againand code has to be reloadedeven if just a single function in the package was updated.
Fortunately,Revise.jl exists.Revise.jl provides massive quality of life improvementsfor package developersby allowing changes to the package source codeto 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 mechanismfor redefining typesthat Revise.jl hooks into,removing arguably the most significant remaining pain pointof Julia package development.The number of times a developer needs to restart Juliawill decrease significantlywith Julia 1.12,allowing for greater productivity.
New Way to Fix Function Arguments
Julia 1.12 introduces the Fix structthat 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 Fixis purely for convenience,particularly when creating closuresof 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 + massend
Now,suppose we have another functionthat computes(or accepts as input)a particular value for massand then creates a new functionthat computes dynamics with mass fixed.Previously,an anonymous function typically would be used,so let’s compare using an anonymous functionto 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 bothdynamics_with_massand dynamics_with_mass_fixact as a function of six argumentsand compute dynamics with mass fixedto a specific value.
Next,suppose we want to compute the derivative of dynamicsas a function of velocityfor 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 nestedto fix multiple arguments.Let’s compare using an anonymous functionto using Fix:
# Fixed values, either computed or given.resistance = 2gravity = 9.8position = 0friction = 0length = 0.5mass = 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 functionis clearer to read.However,Fix can have some performance advantagesover anonymous functions.For example,using Fix can reduce the need to compileanonymous functions that are used repeatedly.
For a lot more context and discussion,see the pull requestthat introduced Fix.
Progress Towards More Reasonable Executables
Perhaps surprisingly,an experimental featureis probably the most highly anticipated addition in Julia 1.12:a new command-line argument --trimthat enables a (currently experimental) featurethat removes unnecessary codefrom compiled executables.
Wait, Julia can generate standalone executables?Yes, though, again, it’s experimental,this time through a script called juliac.jl.(Surprisingly,the release notes say nothing about this script,though it is mentionedin a few threads on Julia Discourse,of which the most helpful to me was this thread.)
Let’s see how to use juliac.jlwith a simple program:
# main.jlfunction @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 0end
This program simply prints to the consoleall its given command-line arguments.
To compile it into an executable,just run the following command:
julia ~/.julia/juliaup/julia-1.12.0+0.x64.linux.gnu/share/julia/juliac/juliac.jl --output-exe print_args --experimental --trim main.jl
(Note that I tested thiswith Julia installed via juliaupon Linux.The location of the juliac.jl scriptmay be different on your system.)
To verify it works:
$ ./print_args hello worldhelloworld
So,what exactly is the impact of --trim?Without the new (experimental) feature,the size of the print_args executablewas 206 MB on my computer.With --trim,that number improved by over 100x,dropping to just 1.6 MB!
Summary
In this post,we learned aboutsome of the new featuresand improvementsintroduced in Julia 1.12,including redefinable types,Fix,and --trim.Curious readers cancheck out the release notesfor the full list of changes.
What are you most excited aboutin Julia 1.12?Let us know in the comments below!
Additional Links
- Julia v1.12 Release Notes
- Full list of changes made in Julia 1.12.
- Julia Basics for Programmers
- Series of blog posts covering Julia basics.
- Diving into Julia
- Series of blog posts covering more advanced Julia concepts.