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 viainclude("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 withusing
.
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
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
:
-
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
-
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 thoughfunc
is exported fromMyModule
,
we created a differentfunc
function
in the REPL
because we did not importfunc
or use a qualified name.
As a result,
future uses offunc
fromMyModule
must use its qualified name.
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 namedMain
is created,
and code that runs
that isn’t explicitly contained in a module
(e.g., code in the REPL)
is executed withinMain
.Base
:
Much of Julia’s basic functionality,
including functions like
+
,print
, andgetindex
,
is defined in a module namedBase
.
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 namedCore
.
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:
x
is not already a local variable.- All enclosing local scopes are soft.
(To illustrate,
nestedfor
loops within the REPL
would satisfy this condition,
while afor
loop in a function
would not.) - 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.)
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
- Julia Documentation: Modules
- Additional information about modules.
- Julia Documentation: Variable Scope
- Additional information about variable scope.