Julia 1.11: Top Features and Important Updates

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.

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 return 6 in Julia 1.11because sum 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 publicare 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 mainafter running the code in the script.main will be passed a Vector of Stringscontaining 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 mainwith 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 mainwill 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 mainwill be definedand then main will be calledwith the command line argumentsas the input.To illustrate:

  • julia test.jl will call main(String[])(because no command line arguments were passed).
  • julia test.jl 1 hello will call main(["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 imports script2.pyand script2.py has the “if main” check,running script1.py as a scriptwill not run script2.py‘s “if main” code.
  • In Julia,if script1.jl includes script2.jland script2.jl uses @main,running script1.jl as a scriptwill run script2.jl‘s main function.(Technicality:Unless script1.jl defines an appropriate main method,in which case script1.jl‘s main would be called,even if script1.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:

Slide about StyledStrings

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 exportas 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 @timedwhen @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 @timeto a log file.I couldn’t just use redirect_stdiobecause that would also redirect the outputof the code being timed.I ended up using @timed along with Base.time_printto 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?

  1. log.(range(exp(a), exp(b), N))
  2. 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 exps and logs.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

]]>