Author Archives: Great Lakes Consulting

Exploring Julia 1.10 – Key Features and Updates

By: Great Lakes Consulting

Re-posted from: https://blog.glcs.io/julia-1-10

A new version of the Julia programming languagewas just released!Version 1.10 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 Latency, or Getting Started Faster

Julia 1.10 has improved latency,which means you can get started faster.

Two sources of latencyhistorically have been slow in Julia:package loadingand just-in-time code compilation.A classic example where this latency was readily noticeablewas when trying to create a plot;consequently,this latency often is calledthe time to first plot (TTFP),or how long one has to waitbefore seeing a plot.

Note that the TTFP issue exists in the first placebecause Julia was designedwith a trade-off in mind:by taking the time to compile a functionthe first time it is called,subsequent calls to the functioncan run at speeds comparable to C.This, however, leads to increased latencyon the first call.

Recent Julia versions have been tackling this issue,and Julia 1.10 further improves latency.

Below is a screenshot of a slide sharedduring the State of Julia talk at JuliaCon 2023.It shows how the time it takesto load Plots.jland then call plotdecreases when moving from Julia 1.8to Julia 1.9and then to Julia 1.10(in this case, Julia 1.10wasn’t released yet,so the alpha version was used).

Improved latency

I saw similar results on my computercomparing Julia 1.9.4 to Julia 1.10.0-rc1(the first release candidate of Julia 1.10):

# Julia 1.9.4julia> @time using Plots  1.278046 seconds (3.39 M allocations: 194.392 MiB, 10.10% gc time, 6.28% compilation time: 89% of which was recompilation)julia> @time display(plot(1:10))  0.365514 seconds (246.08 k allocations: 16.338 MiB, 58.76% compilation time: 10% of which was recompilation)# Julia 1.10.0-rc1julia> @time using Plots  0.713279 seconds (1.42 M allocations: 97.684 MiB, 3.30% gc time, 15.26% compilation time: 86% of which was recompilation)julia> @time display(plot(1:10))  0.257097 seconds (247.72 k allocations: 17.621 MiB, 6.29% gc time, 81.56% compilation time: 9% of which was recompilation)

It’s amazing how much latencyhas been improved!

Better Error Messages

Julia 1.10 now uses JuliaSyntax.jlas the default parser,replacing the old Lisp-based parser.

Having a new parserdoesn’t change how the language runs,but the new parserdoes improve error messages,enabling easier debuggingand creating a lower barrier to entryfor new Julia users.

As an example,consider the following buggy code:

julia> count = 0;julia> for i = 1:10           count++       end

Can you spot the error?

Julia 1.9 gives the following error message:

ERROR: syntax: unexpected "end"

Julia 1.10 gives the following:

ERROR: ParseError:# Error @ REPL[2]:3:1    count++end  invalid identifier

There are at least three improvementsto the error message:

  1. The file location of the offending tokenis prominently displayed.(REPL[2]:3:1 meansthe second REPL entry,the third line,and the first character.This would be replacedwith a file path and line and character numbersif the code were run in a file.)
  2. The specific offending tokenis pointed out with some context.
  3. It is now clear that an identifier(i.e., a variable name)was expectedafter count++.(Note that ++ is a user-definableinfix operator in Julia;so just as a + end is an error,so too is count ++ end.)

Improved error messagesare certainly a welcome addition!

Multithreaded Garbage Collection

Part of Julia’s garbage collectionis now parallelizedin Julia 1.10,resulting in faster garbage collection.

Below is a screenshot of a slide sharedduring the State of Julia talk at JuliaCon 2023.It shows the percentage of timea piece of code spentdoing garbage collectionin different Julia versions(here the master branch is a pre-release version of Julia 1.10).The takeaway is that using threadsdecreased garbage collection time!

Faster garbage collection

The parallelization is implementedusing threads,and the number of threadsavailable for garbage collectioncan be specified when starting Juliawith the command line argument --gcthreads.For example,to use four threads for garbage collection:

julia --gcthreads=4

By default,--gcthreads is halfthe total number of threadsJulia is started with.

Experiment with different numbersof garbage collection threadsto see what works bestfor your code.

Timing Package Precompilation

Timing how long individual packages take to precompileis now easily achieved withPkg.precompile(timing = true).

In Julia 1.9,Pkg.precompile reported just the overall time precompilation took:

julia> using Pkg; Pkg.precompile()Precompiling project...  20 dependencies successfully precompiled in 91 seconds. 216 already precompiled.

Pkg.precompile()(without the timing option)behaves the same in Julia 1.10.But now there is the optionto report the precompilation timefor individual packages:

julia> using Pkg; Pkg.precompile(timing = true)Precompiling project...  19850.9 ms   DataFrames   2858.4 ms   Flux  26206.5 ms   Plots  3 dependencies successfully precompiled in 49 seconds. 235 already precompiled.

Now it is easyto see what packagesprecompile faster than others!

Broadcasting Defined for CartesianIndex

Julia 1.10 now defines broadcastingfor CartesianIndex objects.

A CartesianIndex is a wayto represent an indexinto a multidimensional arrayand can be useful forworking with loops over arrays of arbitrary dimensionality.

Suppose we define the following:

julia> indices = [CartesianIndex(2, 3), CartesianIndex(4, 5)];julia> I = CartesianIndex(1, 1);

In Julia 1.9,attempting to broadcast over a CartesianIndex(for example, indices .+ I)resulted in the following error:

ERROR: iteration is deliberately unsupported for CartesianIndex.

With broadcasting defined,where previously we would have to wrapthe CartesianIndex in a Tuple(e.g., indices .+ (I,)),now the following works:

julia> indices .+ I2-element Vector{CartesianIndex{2}}: CartesianIndex(3, 4) CartesianIndex(5, 6)

Summary

In this post,we learned aboutsome of the new featuresand improvementsintroduced in Julia 1.10.Curious readers cancheck out the release notesfor the full list of changes.

What are you most excited aboutin Julia 1.10?Let us know in the comments below!

Additional Links

Delving into Open Source Packages for Julia

By: Great Lakes Consulting

Re-posted from: https://blog.glcs.io/learning-packages

Julia is a relatively new,free, and open-source programming language.It has a syntaxsimilar to that of other popular programming languagessuch as MATLAB and Python,but it boasts being able to achieve C-like speeds.

Julia comeswith a lot of functionality built-in.However, functionalitythat isn’t already built-inneeds to be createdfrom base Julia.Fortunately,Julia provides a simple, yet powerful, mechanismfor reusing and sharing code:packages.

Thanks to packages,we don’t have to write the codeto do many common tasks ourselves.(Imagine having to write a plotting function from scratch…)

We learned in a previous post about the Pkg REPL promptand how it can be used to install packages.

To install a package,you have to know it exists.And when using a packagefor the first time,how do you know where to begin?The purpose of this postis to address this question.

In this post,we will learn how to find useful packagesand demonstrate how to discovera package’s functionalityand learn how to use it.

This post assumes you already havea basic understanding of variables and functionsin Julia.You should also understand the differencebetween functions and methods.If you haven’t yet,check out our earlierpost on variables and functionsas well as our post on multiple dispatch,which explains the differencebetween functions and methods.

Package Discovery

The first step to learning a Julia packageis actually finding the package.

Essentially all Julia packages are registered,or made available for downloadvia the Pkg REPL prompt,in Julia’s General registry.Therefore, one could look through the registryto get a sense of the different packages available.

For a more powerful way to explore Julia packages,check out juliapackages.com.This website gathers information from GitHubto allow sorting by popularity(as measured by the number of GitHub stars)and when packages were last updated(which can help give a senseof how actively maintained or updated packages are).You can also explore packages by category.

Screenshot of juliapackages.com

Finally,another way to discover packagesis to visit Julia Discourse.You can look at package announcementsto see what packages are being created.You can also peruse the specific domains tagsto see what packages people are talking aboutand get a feel for what packages people usefor different applications.

Now that we have some toolsfor discovering packages,let’s discuss how to learnhow to use a package.

Learning Package Functionality

Look at Documentation

The first step to finding outwhat a package has to offeris to look at the package’s documentation.

Picture of an open book

Most packages will have at least a READMEthat will list package functionalityand provide some examplesof how to use the package.See Interpolation.jl’s READMEas an example.

Often, more established packageswill also have dedicated documentation(that typically is linkedin the README).Documentation typically includesmore in-depth examplesof how to perform specific tasksusing the package.For example,DataFrames.jl includes a “First Steps” pagein its documentation.

Another common feature of package documentationis a list of functions, types, constants, and other symbolsdefined by the package.See, for example,ForwardDiff.jl’s differentiation API.This list can be usefulfor discovering all possible package functionality,especially when the examples elsewhere in the documentationcover only a small portionof package functionality.

Explore in the REPL

Besides looking at online documentation,the REPL can also be usefulfor learning how to use a package.

After a package is loaded,an exhaustive list of symbolsdefined in a packagecan be obtained via tab completion:

julia> using Debuggerjulia> Debugger.<tab><tab>@bp                             @breakpoint                     @enter@make_frame                     @run                            DebugCompletionProviderDebuggerState                   HIGHLIGHT_24_BIT                HIGHLIGHT_256_COLORSHIGHLIGHT_OFF                   HIGHLIGHT_SYSTEM_COLORS         HighlightOptionLimitIO                         LimitIOException                LineNumbersMAX_BYTES_REPR                  NUM_SOURCE_LINES_UP_DOWN        RESETRunDebugger                     SEARCH_PATH                     WATCH_LIST__init__                        _current_theme                  _eval_code_iscall                         _isdotcall                      _make_frame_preprocess_enter               _print_full_path                _syntax_highlightingactive_frame                    add_breakpoint!                 add_watch_entry!append_any                      assert_allow_step               body_for_methodbreak_off                       break_on                        break_on_errorbreakpoint                      breakpoint_char                 breakpoint_linenumberscheck_breakpoint_index          clear_watch_list!               completionscompute_source_offsets          disable_breakpoint!             enable_breakpoint!eval                            execute_command                 get_function_in_module_or_Mainhighlight_code                  include                         interpret_variableinvalid_command                 julia_prompt                    locdesclocinfo                         maybe_quote                     parse_as_much_as_possiblepattern_match_apply_call        pattern_match_kw_call           print_codeinfoprint_frame                     print_lines                     print_localsprint_next_expr                 print_sourcecode                print_statusprint_var                       promptname                      remove_breakpoint!repr_limited                    set_highlight                   set_themeshow_breakpoint                 show_breakpoints                show_watch_liststacklength                     suppressed                      toggle_breakpoint!toggle_lowered                  toggle_mode                     write_prompt

As discussed in ourpost about the Julia REPL,the help prompt can be usedto display documentationfor individual functions and types:

# Press ? to enter help modehelp?> filtersearch: filter filter! fieldtype fieldtypes  filter(f, a)  Return a copy of collection a, removing elements for which f is false.  The function f is passed one argument.

If you want to find outwhat methods existfor a given function,you can use tab completion:

julia> print(<tab>print(io::IO, ex::Union{Core.GotoNode, Core.SSAValue, Expr, GlobalRef, Core.GotoIfNot, LineNumberNode, Core.PhiCNode, Core.PhiNode, QuoteNode, Core.ReturnNode, Core.Slot, Core.UpsilonNode}) @ Base show.jl:1384print(io::IO, s::Union{SubString{String}, String}) @ Base strings/io.jl:246print(io::IO, x::Union{Float16, Float32}) @ Base.Ryu ryu/Ryu.jl:128print(io::IO, n::Unsigned) @ Base show.jl:1144

You can also use the methods function:

julia> methods(print)# 35 methods for generic function "print" from Base:  [1] print(io::IO, ex::Union{Core.GotoNode, Core.SSAValue, Expr, GlobalRef, Core.GotoIfNot, LineNumberNode, Core.PhiCNode, Core.PhiNode, QuoteNode, Core.ReturnNode, Core.Slot, Core.UpsilonNode})     @ show.jl:1384  [2] print(io::IO, s::Union{SubString{String}, String})     @ strings/io.jl:246  [3] print(io::IO, x::Union{Float16, Float32})     @ Base.Ryu ryu/Ryu.jl:128  [4] print(io::IO, n::Unsigned)     @ show.jl:1144  

The methods functionalso allows filteringon input typesand on the modulein which the methods are defined.For example,to get a list of methods of printthat take two arguments,the second of which is an AbstractChar:

julia> methods(print, (Any, AbstractChar))# 5 methods for generic function "print" from Base: [1] print(io::IO, c::Char)     @ char.jl:252 [2] print(io::IO, c::AbstractChar)     @ char.jl:253 [3] print(io::IO, x)     @ strings/io.jl:32 [4] print(io::IO, xs...)     @ strings/io.jl:42 [5] print(xs...)     @ coreio.jl:3

(See also the related methodswith function.)

And to get methods of printdefined in the Dates package:

julia> using Datesjulia> methods(print, Dates)# 4 methods for generic function "print" from Base: [1] print(io::IO, x::Period)     @ Dates ~/programs/julia/julia-1.9.4/share/julia/stdlib/v1.9/Dates/src/periods.jl:48 [2] print(io::IO, t::Time)     @ Dates ~/programs/julia/julia-1.9.4/share/julia/stdlib/v1.9/Dates/src/io.jl:55 [3] print(io::IO, dt::Date)     @ Dates ~/programs/julia/julia-1.9.4/share/julia/stdlib/v1.9/Dates/src/io.jl:714 [4] print(io::IO, dt::DateTime)     @ Dates ~/programs/julia/julia-1.9.4/share/julia/stdlib/v1.9/Dates/src/io.jl:705

As another example,suppose you have a DataFrameand want to use the groupby functionbut aren’t sure what other argumentsgroupby expects.Tab completion (or the methods function) can help:

julia> using DataFramesjulia> x = DataFrame(a = 1:3, b = rand(3));julia> gx = groupby(x, <tab>groupby(df::AbstractDataFrame, cols; sort, skipmissing) @ DataFrames ~/.julia/packages/DataFrames/58MUJ/src/groupeddataframe/groupeddataframe.jl:218

After running a package’s function,you might want to learn more aboutwhat the function returned.The typeof functionreturns the type of its input,and fieldnamesreturns a list of propertiesthat can be accessed:

julia> d = Dict("a" => 1, "b" => 2)Dict{String, Int64} with 2 entries:  "b" => 2  "a" => 1julia> x = collect(d)2-element Vector{Pair{String, Int64}}: "b" => 2 "a" => 1julia> typeof(x)Vector{Pair{String, Int64}} (alias for Array{Pair{String, Int64}, 1})julia> fieldnames(typeof(x[1]))(:first, :second)julia> x[1].first"b"

Tab completion can also be usedto list properties:

julia> x[1].<tab><tab>first   second

You can also see where in the type hierarchyan object’s type lieswith the supertype function:

julia> supertype(typeof(x))DenseVector{Pair{String, Int64}} (alias for DenseArray{Pair{String, Int64}, 1})

Picture of source code

Read Source Code

In addition to reading documentationand experimenting in the REPL,sometimes the best way to learn a packageis to read the source code directly.While that may seem daunting at first,remember that Julia is a high-level language,making it somewhat easy to read(at least after getting used to it).

There are two main benefitsof reading the source code:

  1. You get to see how the packagecreates and usescustom types and functions.
  2. Typically code is organizedin a logical manner,so you get to see what symbolslogically belong together.For example,a file that defines a typetypically will also defineconstructors for that typeand functions that operate on it.

If you see a function calland want to know what method will be called,the @which command in the REPL can help.For example:

julia> using DataFramesjulia> df = DataFrame(a = 1:3);julia> @which hcat(df, df)hcat(df1::AbstractDataFrame, df2::AbstractDataFrame; makeunique, copycols)     @ DataFrames ~/.julia/packages/DataFrames/58MUJ/src/abstractdataframe/abstractdataframe.jl:1608

Now we know the file and line numberwhere the hcat methodthat acts on two DataFrames is defined,and we can look at the source codeto learn more about what the method does.

If you don’t have any objects to work withbut know the types of the inputs,you can use the which method instead:

julia> which(hcat, (DataFrame, DataFrame))hcat(df1::AbstractDataFrame, df2::AbstractDataFrame; makeunique, copycols)     @ DataFrames ~/.julia/packages/DataFrames/58MUJ/src/abstractdataframe/abstractdataframe.jl:1608

Summary

That wraps up our discussionabout how to find useful packagesand how to discover and learn to usea package’s functionality.We listed a few tools for finding packagesand walked through some different methodsfor learning how to use a package,including looking at documentation,exploring in the REPL,and reading source code.

Do you have any tips or tricksfor learning how to use packages in Julia?Let us know in the comments below!

Additional Links

Exploring Modules and Variable Scope in Julia Programming

By: Great Lakes Consulting

Re-posted from: https://blog.glcs.io/modules-variable-scope

Julia is a relatively new,
free, and open-source programming language.
It has a syntax
similar to that of other popular programming languages
such as MATLAB and Python,
but it boasts being able to achieve C-like speeds.

One way to organize Julia code
is to split functionality
into individual functions.
When enough functions exist,
it may become useful
to group the functions together,
along with any relevant global variables,
constants,
and type definitions.
Julia provides modules for this purpose.

Modules form the backbone of Julia packages,
helping to organize code
and minimize namespace collisions.

In this post,
we will learn about
modules in Julia,
and we will discuss how to create and use them.
Because modules each have their own global scope,
we will also learn about
scoping rules for variables.

This post assumes you already have
a basic understanding of variables and functions
in Julia.
You should also understand the difference
between functions and methods.
If you haven’t yet,
check out our earlier
post on variables and functions
as well as our post on multiple dispatch,
which explains the difference
between functions and methods.

Modules

The syntax for creating a module is

module ModuleName

# Code goes here.

end

Here’s an example module:

module MyModule

using Statistics

const A = "A global constant"
const B = [1, 2, 3]

func(x) = println("A: ", A, "\nmean(B): ", mean(B), "\nx: ", x)

export A, func

end

Let’s walk through this code.

  • First,
    the module loads another package:

    using Statistics
    

    Modules can load packages,
    just like we can do in the REPL.
    When a package is loaded in a module,
    the package is brought into the module’s namespace,
    meaning the loaded symbols
    (i.e., names referring to functions, types, constants, etc.)
    are not visible outside of the module.
    For example:

    julia> module StatsModule
           using Statistics
           end
    Main.StatsModule
    
    julia> mean([1, 2, 3])
    ERROR: UndefVarError: `mean` not defined
    
  • Next,
    the module defines its own data and functionality:

    const A = "A global constant"
    const B = [1, 2, 3]
    
    func(x) = println("A: ", A, "\nmean(B): ", mean(B), "\nx: ", x)
    

    This is the code
    we want to organize into a module.
    Typically,
    there are more lines of code,
    and they are saved in one or more separate files
    that are just included by the module via

    include("mycode.jl")
    
  • Finally,
    the module specifies some exports:

    export A, func
    

    Modules can export symbols
    that are then made available
    when the module is loaded with using.

Using Modules

When a module is created,
it can be referred to
by its name,
and any symbols in its namespace
can be accessed
by prepending the module name,
e.g., MyModule.func.
(This is called a qualified name.)

julia> MyModule
Main.MyModule

julia> MyModule.func(1)
A: A global constant
mean(B): 2.0
x: 1

If we want to make exported symbols
available without using a qualified name,
we can load the module with using:

julia> using .MyModule

julia> A
"A global constant"

(Here we note one difference
between packages and modules:
packages can be loaded with using PackageName,
whereas modules need their name
to be prepended with a period,
as seen above.)

After loading a module with using,
unexported symbols
are not made directly available,
but they can still be accessed
via a qualified name:

julia> B
ERROR: UndefVarError: `B` not defined

julia> MyModule.B
3-element Vector{Int64}:
 1
 2
 3

If we want to make an unexported symbol
available without using a qualified name,
we can explicitly load it:

julia> using .MyModule: B

julia> B
3-element Vector{Int64}:
 1
 2
 3

import Statements

import is another keyword
that can be used to load modules and packages.

import .MyModule will make available
just the name MyModule,
not any exported symbols:

julia> import .MyModule

julia> func(false) # Error, even though `func` is exported
ERROR: UndefVarError: `func` not defined

julia> MyModule.func(false) # Qualified names still work
A: A global constant
mean(B): 2.0
x: false

Using vs. import

import also allows methods
to be added to a module’s functions
without using a qualified name:

julia> import .MyModule: func

julia> func() = println("Method 2")
func (generic function with 2 methods)

julia> func()
Method 2

julia> func("MyModule.func")
A: A global constant
mean(B): 2.0
x: MyModule.func

For comparison,
below are two similar examples that use using:

  1. julia> using .MyModule: func
    
    julia> func() = println("Method 2")
    ERROR: error in method definition: function MyModule.func
    must be explicitly imported to be extended
    

    Here,
    we learn that we cannot add a method
    to a function from another module
    without importing the function,
    as we did earlier,
    or referring to the function
    with its qualified name,
    as shown below:

    julia> using .MyModule
    
    julia> MyModule.func() = println("Method 2")
    
    julia> func()
    Method 2
    
    julia> func("MyModule.func")
    A: A global constant
    mean(B): 2.0
    x: MyModule.func
    
  2. julia> using .MyModule
    
    julia> func() = println("Method 2")
    func (generic function with 1 method)
    
    julia> func()
    Method 2
    
    julia> func("MyModule.func")
    ERROR: MethodError: no method matching func(::String)
    
    julia> MyModule.func("MyModule.func")
    A: A global constant
    mean(B): 2.0
    x: MyModule.func
    

    Here,
    we see that,
    even though func is exported from MyModule,
    we created a different func function
    in the REPL
    because we did not import func
    or use a qualified name.
    As a result,
    future uses of func from MyModule
    must use its qualified name.

Adding methods to a module's function

Finally,
import enables renaming symbols:

julia> import .MyModule as MM

julia> MM.A
"A global constant"

julia> import .MyModule: B as NEWNAME

julia> NEWNAME
3-element Vector{Int64}:
 1
 2
 3

Common Modules

Now that we know how modules work,
let’s learn about a few modules
that every Julia programmer will come across:
Main, Base and Core.

  • Main:
    It turns out that all Julia code
    executes within a module.
    When Julia starts,
    a module named Main is created,
    and code that runs
    that isn’t explicitly contained in a module
    (e.g., code in the REPL)
    is executed within Main.
  • Base:
    Much of Julia’s basic functionality,
    including functions like
    +, print, and getindex,
    is defined in a module named Base.
    This module is automatically loaded
    into all modules
    (with few exceptions).
  • Core:
    Code that is considered built-in to Julia,
    i.e., code Julia needs to be able to function,
    lives in a module named Core.
    This module also is automatically loaded
    into all modules
    (with even fewer exceptions).

Variable Scope

Variable scope refers to where in code
a variable is accessible.
It therefore has implications
for when two pieces of code
can use the same variable name
without referring to the same thing.

There are three types of scopes in Julia:
global scope, hard local scope, and soft local scope.
We will discuss each of these in turn.

Global Scope

Symbols defined within a global scope
can be accessed within the global scope
and any local scopes
contained in the global scope.

Each module defines its own global scope.
Importantly,
there is no universal global scope,
meaning there is nowhere
we can define, e.g., x
and have x refer to the same thing everywhere,
even across modules.

Also note that global scopes do not nest,
in the sense that a nested module
cannot refer to a containing module’s global variable:

julia> module A
           a = 1
           module B
               b = a # `a` is undefined here, even though `B` is nested within `A`
           end
       end
ERROR: UndefVarError: `a` not defined

Hard Local Scope

Symbols defined within a hard local scope
can be accessed within the local scope
and any contained local scopes.

Functions, let blocks, and comprehensions
each introduce a hard local scope.

In a hard local scope,
variable assignment always assigns
to a local variable
unless the variable is explicitly declared as global
using the global keyword:

julia> let
           x = 1 # Assigns to a local variable `x`
       end;

julia> x
ERROR: UndefVarError: `x` not defined

julia> let
           global x
           x = 1 # Assigns to a global variable `x`
       end;

julia> x
1

Furthermore,
if there is a local variable, e.g., x,
in an outer local scope,
assignment to x in an inner local scope
will assign to the same x
in the outer local scope:

julia> let
           x = 0
           for i = 1:10
               s = x + i
               x = s # `x` is the existing local variable, not a new one
           end
           x # 55, not 0
       end
55

In particular,
it will not create a new local named x
unless x is explicitly declared local
in the inner scope.
This is called shadowing,
where names can be reused
to refer to different things:

julia> x = 1; # A global `x`

julia> let
           x = 2 # A local `x` shadowing the global `x`
           let
               local x
               x = 3 # An inner local `x` shadowing the outer local `x`
               @show x # Shows `x = 3`
           end
           @show x # Shows `x = 2`, not `x = 3`
       end;
x = 3
x = 2

julia> @show x; # Shows `x = 1`, not `x = 2` or `x = 3`
x = 1

Another example of shadowing:

julia> x = 1; # A global `x`

julia> function f(x)
           x + 1 # This `x` refers to the local `x`, not the global one
       end;

julia> f(3) # Computes `3 + 1`, not `1 + 1`
4

Soft Local Scope

for, while, and try blocks
each introduce a soft local scope.

Soft local scope is the same as hard local scope
except in interactive contexts
(e.g., when running code in the REPL)
and when assigning to a variable
(let’s call it x)
while the following conditions are met:

  1. x is not already a local variable.
  2. All enclosing local scopes are soft.
    (To illustrate,
    nested for loops within the REPL
    would satisfy this condition,
    while a for loop in a function
    would not.)
  3. A global variable x is defined.

In this case,
the global variable x is assigned
(as opposed to creating a new local variable x,
as would be done in a hard local scope).
(See the Julia documentation
for the rationale.)

Variable scope

Summary

In this post,
we learned how to create and use modules in Julia
for organizing code.
We also learned about Julia’s scoping rules
for global, hard local, and soft local scopes.

How do you use modules in your code?
Let us know in the comments below!

Additional Links