Julia 1.11: Top Features and Important Updates
A new version of the Julia programming language was just released! Version 1.11 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.
- Improved Clarity of Public API with
public
Keyword - Standardized Entry Point for Scripts
- Improved String Styling
- New Functions for Testing Existence of Documentation
- More Complete
@timed
Macro - New Convenience Function
logrange
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 Clarity of Public API with public
Keyword
An important aspect of semantic versioning (aka SemVer, which Julia and Julia packages follow) is the public API users have access to. In essence, SemVer states that minor package updates should not break compatibility with existing code that uses the public API. However, other parts of the code are free to change in a minor update. To illustrate:
- If I have
sum([1, 2, 3])
in my code that I wrote in Julia 1.10, it will continue to return6
in Julia 1.11 becausesum
is part of Julia's public API. But it could break in Julia 2.0. (Hopefully not, though!) - If I have
SomePackage._internal_function(0)
in my code that I wrote with SomePackage v1.2.0, it might error when SomePackage upgrades to v1.2.1 (because, for example,_internal_function
got deleted). Such a change would be allowed because_internal_function
is not part of the public API.
So, the question is, how does a user know what the public API of a package is? Historically, there have been some conventions followed:
- Names that are exported
(e.g.,
export DataFrame
) are part of the public API. - Unexported names that are prefixed with an underscore
(e.g.,
SomePackage._internal_function
) are not part of the public API.
But what about a function like ForwardDiff.gradient
?
That function is the reason why 99%
of users load the ForwardDiff package,
but it's not exported!
The good news is that it's still part of the public API
because, well, ForwardDiff's maintainers say so.
Or maybe it's because the documentation says so.
Or maybe it's because enough people use it?
Sometimes it's not entirely clear.
But now in Julia 1.11,
your the code can indicate
the public API!
This is thanks to a new public
keyword.
Now,
all symbols that are documented with public
are part of the public API.
(Note that this is in addition to exported symbols,
i.e., func
would be considered public API
with either export func
or public func
.)
Usage of public
is the same as export
.
For example:
module MyPackage
public add1
"""
Docstring for a public function.
"""
add1(x) = x + 1
"""
Docstring for a private function.
"""
private_add1(x) = x + 1
end
With using MyPackage
,
no symbols are made available (except MyPackage
),
e.g., add1
can only be called via MyPackage.add1
,
because nothing is exported with export
.
And both MyPackage.add1(1)
and MyPackage.private_add1(1)
work,
even though add1
is public and private_add1
is private.
So,
the public
keyword doesn't change how MyPackage works or is used.
However,
the public
keyword does change some behaviors.
The most notable difference is
when displaying documentation in the REPL's help mode:
help?> MyPackage.add1
Docstring for a public function.
help?> MyPackage.private_add1
│ Warning
│
│ The following bindings may be internal; they may change or be removed in future versions:
│
│ • MyPackage.private_add1
Docstring for a private function.
See the warning with private_add1
?
Such warnings,
in addition to the more straightforward documentation of the public API,
may help reduce usage of package internals
(particularly accidental usage),
which in turn may help improve
the stability of Julia packages.
To summarize,
even though the public
keyword
doesn't change how a package works or is used,
it does provide a mechanism for clearly stating the public API
and providing warnings when viewing the documentation
for internal functions.
This, in turn,
may improve the stability of Julia packages
as they adhere more closely
to public APIs.
Read more about public
in the Julia manual.
Standardized Entry Point for Scripts
There is now a standardized entry point when running scripts from the command line.
Julia 1.11 introduces the @main
macro.
This macro, when placed in a script,
and when the script is run via the command line,
tells Julia to run a function called main
after running the code in the script.
main
will be passed a Vector
of String
s
containing the command line arguments passed to the script.
To use @main
,
just include it in your script
on its own line
after defining your main
function.
(If @main
occurs before defining main
,
for example at the top of the script,
an error will be thrown,
so the ordering matters.)
Of course,
for this to work
there has to be a function main
with a method that takes a Vector{String}
as the only input.
Let's look at an example
to illustrate how this works.
Say we have the following in test.jl
:
print_val(x) = print(x)
function main(args)
print_val(args)
end
@main
If we run this file in the REPL
with include("test.jl")
,
the functions print_val
and main
will be defined,
but main
will not get called.
This is the same behavior
as when @main
is not present.
On the other hand,
if we run this file via the command line
with julia test.jl
,
the functions print_val
and main
will be defined
and then main
will be called
with the command line arguments
as the input.
To illustrate:
julia test.jl
will callmain(String[])
(because no command line arguments were passed).julia test.jl 1 hello
will callmain(["1", "hello"])
.
As a result of @main
,
a Julia file can have different behavior
depending on whether it is run as a script or not.
If you're familiar with Python,
@main
might remind you of
if __name__ == "__main__"
.
However,
there is one significant difference:
- In Python,
if
script1.py
importsscript2.py
andscript2.py
has the "if main" check, runningscript1.py
as a script will not runscript2.py
's "if main" code. - In Julia,
if
script1.jl
includesscript2.jl
andscript2.jl
uses@main
, runningscript1.jl
as a script will runscript2.jl
'smain
function. (Technicality: Unlessscript1.jl
defines an appropriatemain
method, in which casescript1.jl
'smain
would be called, even ifscript1.jl
did not include@main
.)
This isn't to say Julia's @main
is bad or wrong;
it's just important to know that it works differently
than Python.
And it's still cool to have a standardized entry point
for Julia scripts now!
Read more about @main
in the Julia manual.
Improved String Styling
Julia 1.11 introduces a new StyledStrings.jl
standard library package.
This package provides a convenient way
to add styling to strings.
StyledStrings makes printing styled strings
much easier than calling printstyled
,
particularly when different parts of the string
have different styles.
The easiest way to create a styled string
is with styled"..."
.
For example:
using StyledStrings
styled_string = styled"{italic:This} is a {bold,bright_cyan:styled string}!"
Then, when printing the styled string, it will display according to the provided annotations.
Also, because the style information is stored with the string, it can easily be preserved across string manipulations such as string concatenation or grabbing a substring.
Check out the documentation for more information about the variety of different annotations StyledStrings supports.
And here's some more information from the State of Julia talk at JuliaCon 2024:
New Functions for Testing Existence of Documentation
Julia 1.11 makes it easy to determine programmatically whether a function has a docstring. This can be useful for, e.g., CI checks to ensure a package is well documented.
There are two functions for this purpose.
The first is Docs.hasdoc
,
which is used to query a particular function.
hasdoc
takes two inputs:
the module to look in
and the name (as a Symbol
) of the function.
For example:
julia> Docs.hasdoc(Base, :sum)
true
The other function provided
is Docs.undocumented_names
,
which returns a list of a module's public names
that have no docstrings.
(Note that public names include symbols exported via export
as well as symbols declared as public via public
.)
For example:
julia> module Example
export f1, f4
public f2, f5
"Exported, documented"
f1() = 1
"Public, documented"
f2() = 2
"Internal, documented"
f3() = 3
# Exported, undocumented
f4() = 4
# Public, undocumented
f5() = 5
# Internal, undocumented
f6() = 6
end
Main.Example
# Note that `f6` is not returned because it is neither exported nor public.
julia> Docs.undocumented_names(Example)
3-element Vector{Symbol}:
:Example
:f4
:f5
It will be interesting to see what tooling arises to take advantage of these functions.
More Complete timed
Macro
The @timed
macro
provides more complete timing information
in Julia 1.11.
Previously,
@timed
gave run time and allocation/garbage collection information,
but nothing about compilation time.
Now, compilation time is included.
But why care about @timed
when @time
already gave all that info?
Because @time
is hard-coded to print to stdout
,
meaning there's no way to capture the information,
e.g., for logging purposes.
I actually had a project where I wanted to redirect the output of @time
to a log file.
I couldn't just use redirect_stdio
because that would also redirect the output
of the code being timed.
I ended up using @timed
along with Base.time_print
to create the log statements,
but I was disappointed @timed
didn't give me compilation time information.
Well, now it does!
New Convenience Function logrange
Pop quiz: Which of the following is the correct way to create a logarithmically spaced range of numbers?
log.(range(exp(a), exp(b), N))
exp.(range(log(a), log(b), N))
I have occasionally needed
to use logarithmically spaced ranges of numbers,
not so frequently that I memorized which expression to use,
but frequently enough that I developed a real distaste
for the mental gymnastics I had to go through
every time just to remember
where to put the exp
s and log
s.
Maybe I should have just taken some time
to memorize the answer...
But now it doesn't matter!
The correct answer is neither 1 nor 2,
but logrange(a, b, N)
!
Here's an example usage:
julia> logrange(1, 10, 5)
5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
1.0, 1.77828, 3.16228, 5.62341, 10.0
I know it's a fairly minor change,
but the addition of logrange
in Julia 1.11
is probably the change I'm most excited about.
There was much rejoicing when I saw the news!
Summary
In this post, we learned about some of the new features and improvements introduced in Julia 1.11. Curious readers can check out the release notes for the full list of changes.
Note also that with the new release, Julia 1.10 will now become the LTS (long-term support) version, replacing Julia 1.6. As a result, Julia 1.10 will receive maintenance updates instead of Julia 1.6 (which has now reached end of support) until the next LTS version is announced. If you want to learn more about what changes Julia 1.10 brought, check out our post!
What are you most excited about in Julia 1.11? Let us know in the comments below!
Additional Links
- Julia v1.11 Release Notes
- Full list of changes made in Julia 1.11.
- Julia Basics for Programmers
- Series of blog posts covering Julia basics.
Subscribe to my newsletter
Read articles from Steven Whitaker directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by