By: Steven Whitaker
Re-posted from: https://blog.glcs.io/julia-1-11
A new version of the Julia programming languagewas just released!Version 1.11 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 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 updatesshould not break compatibilitywith existing code that uses the public API.However,other parts of the code are free to changein a minor update.To illustrate:
- If I have
sum([1, 2, 3])
in my codethat I wrote in Julia 1.10,it will continue to return6
in Julia 1.11becausesum
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 codethat 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 allowedbecause_internal_function
is not part of the public API.
So, the question is,how does a user knowwhat 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 APIbecause, 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 indicatethe 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 APIwith either export func
or public func
.)
Usage of public
is the same as export
.For example:
module MyPackagepublic add1"""Docstring for a public function."""add1(x) = x + 1"""Docstring for a private function."""private_add1(x) = x + 1end
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 iswhen 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 improvethe stability of Julia packages.
To summarize,even though the public
keyworddoesn’t change how a package works or is used,it does provide a mechanism for clearly stating the public APIand providing warnings when viewing the documentationfor internal functions.This, in turn,may improve the stability of Julia packagesas they adhere more closelyto public APIs.
Read more about public
in the Julia manual.
Standardized Entry Point for Scripts
There is now a standardized entry pointwhen 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
scontaining the command line arguments passed to the script.
To use @main
,just include it in your scripton its own lineafter 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 workthere has to be a function main
with a method that takes a Vector{String}
as the only input.
Let’s look at an exampleto 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 REPLwith include("test.jl")
,the functions print_val
and main
will be defined,but main
will not get called.This is the same behavioras when @main
is not present.
On the other hand,if we run this file via the command linewith julia test.jl
,the functions print_val
and main
will be definedand then main
will be calledwith the command line argumentsas 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 behaviordepending on whether it is run as a script or not.
If you’re familiar with Python,@main
might remind you ofif __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 scriptwill not runscript2.py
‘s “if main” code. - In Julia,if
script1.jl
includesscript2.jl
andscript2.jl
uses@main
,runningscript1.jl
as a scriptwill 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 differentlythan Python.And it’s still cool to have a standardized entry pointfor Julia scripts now!
Read more about @main
in the Julia manual.
Improved String Styling
Julia 1.11 introduces a new StyledStrings.jlstandard library package.This package provides a convenient wayto add styling to strings.StyledStrings makes printing styled stringsmuch easier than calling printstyled
,particularly when different parts of the stringhave different styles.
The easiest way to create a styled stringis with styled"..."
.For example:
using StyledStringsstyled_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 manipulationssuch as string concatenation or grabbing a substring.
Check out the documentationfor more informationabout the variety of different annotationsStyledStrings supports.
And here’s some more informationfrom the State of Julia talk at JuliaCon 2024:
New Functions for Testing Existence of Documentation
Julia 1.11 makes it easyto determine programmatically whether a function has a docstring.This can be useful for, e.g., CI checksto 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 inand the name (as a Symbol
) of the function.For example:
julia> Docs.hasdoc(Base, :sum)true
The other function providedis Docs.undocumented_names
,which returns a list of a module’s public namesthat 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 endMain.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 arisesto take advantage of these functions.
More Complete timed
Macro
The @timed
macroprovides more complete timing informationin 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 outputof 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 followingis the correct wayto create a logarithmically spaced range of numbers?
log.(range(exp(a), exp(b), N))
exp.(range(log(a), log(b), N))
I have occasionally neededto use logarithmically spaced ranges of numbers,not so frequently that I memorized which expression to use,but frequently enough that I developed a real distastefor the mental gymnastics I had to go throughevery time just to rememberwhere to put the exp
s and log
s.Maybe I should have just taken some timeto 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.11is probably the change I’m most excited about.There was much rejoicing when I saw the news!
Summary
In this post,we learned aboutsome of the new featuresand improvementsintroduced in Julia 1.11.Curious readers cancheck out the release notesfor 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 updatesinstead 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 aboutin 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.