By: Steven Whitaker
Re-posted from: https://glcs.hashnode.dev/modules-variable-scope
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.
One way to organize Julia codeis to split functionalityinto individual functions.When enough functions exist,it may become usefulto 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 codeand minimize namespace collisions.
In this post,we will learn aboutmodules in Julia,and we will discuss how to create and use them.Because modules each have their own global scope,we will also learn aboutscoping rules for variables.
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.
Modules
The syntax for creating a module is
module ModuleName# Code goes here.end
Here’s an example module:
module MyModuleusing Statisticsconst A = "A global constant"const B = [1, 2, 3]func(x) = println("A: ", A, "\nmean(B): ", mean(B), "\nx: ", x)export A, funcend
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 endMain.StatsModulejulia> 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 codewe want to organize into a module.Typically,there are more lines of code,and they are saved in one or more separate filesthat are just included by the module via
include("mycode.jl")
- Finally,the module specifies some exports:
export A, func
Modules can export symbolsthat are then made availablewhen the module is loaded with
using
.
Using Modules
When a module is created,it can be referred toby its name,and any symbols in its namespacecan be accessedby prepending the module name,e.g., MyModule.func
.(This is called a qualified name.)
julia> MyModuleMain.MyModulejulia> MyModule.func(1)A: A global constantmean(B): 2.0x: 1
If we want to make exported symbolsavailable without using a qualified name,we can load the module with using
:
julia> using .MyModulejulia> A"A global constant"
(Here we note one differencebetween packages and modules:packages can be loaded with using PackageName
,whereas modules need their nameto be prepended with a period,as seen above.)
After loading a module with using
,unexported symbolsare not made directly available,but they can still be accessedvia a qualified name:
julia> BERROR: UndefVarError: `B` not definedjulia> MyModule.B3-element Vector{Int64}: 1 2 3
If we want to make an unexported symbolavailable without using a qualified name,we can explicitly load it:
julia> using .MyModule: Bjulia> B3-element Vector{Int64}: 1 2 3
import
Statements
import
is another keywordthat can be used to load modules and packages.
import .MyModule
will make availablejust the name MyModule
,not any exported symbols:
julia> import .MyModulejulia> func(false) # Error, even though `func` is exportedERROR: UndefVarError: `func` not definedjulia> MyModule.func(false) # Qualified names still workA: A global constantmean(B): 2.0x: false
import
also allows methodsto be added to a module’s functionswithout using a qualified name:
julia> import .MyModule: funcjulia> func() = println("Method 2")func (generic function with 2 methods)julia> func()Method 2julia> func("MyModule.func")A: A global constantmean(B): 2.0x: MyModule.func
For comparison,below are two similar examples that use using
:
-
julia> using .MyModule: funcjulia> func() = println("Method 2")ERROR: error in method definition: function MyModule.funcmust be explicitly imported to be extended
Here,we learn that we cannot add a methodto a function from another modulewithout importing the function,as we did earlier,or referring to the functionwith its qualified name,as shown below:
julia> using .MyModulejulia> MyModule.func() = println("Method 2")julia> func()Method 2julia> func("MyModule.func")A: A global constantmean(B): 2.0x: MyModule.func
-
julia> using .MyModulejulia> func() = println("Method 2")func (generic function with 1 method)julia> func()Method 2julia> func("MyModule.func")ERROR: MethodError: no method matching func(::String)julia> MyModule.func("MyModule.func")A: A global constantmean(B): 2.0x: MyModule.func
Here,we see that,even though
func
is exported fromMyModule
,we created a differentfunc
functionin the REPLbecause 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 MMjulia> MM.A"A global constant"julia> import .MyModule: B as NEWNAMEjulia> NEWNAME3-element Vector{Int64}: 1 2 3
Common Modules
Now that we know how modules work,let’s learn about a few modulesthat every Julia programmer will come across:Main
, Base
and Core
.
Main
:It turns out that all Julia codeexecutes within a module.When Julia starts,a module namedMain
is created,and code that runsthat 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 loadedinto 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 loadedinto all modules(with even fewer exceptions).
Variable Scope
Variable scope refers to where in codea variable is accessible.It therefore has implicationsfor when two pieces of codecan use the same variable namewithout 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 scopecan be accessed within the global scopeand any local scopescontained in the global scope.
Each module defines its own global scope.Importantly,there is no universal global scope,meaning there is nowherewe 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 modulecannot 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 endERROR: UndefVarError: `a` not defined
Hard Local Scope
Symbols defined within a hard local scopecan be accessed within the local scopeand any contained local scopes.
Functions, let
blocks, and comprehensionseach introduce a hard local scope.
In a hard local scope,variable assignment always assignsto a local variableunless the variable is explicitly declared as globalusing the global
keyword:
julia> let x = 1 # Assigns to a local variable `x` end;julia> xERROR: UndefVarError: `x` not definedjulia> let global x x = 1 # Assigns to a global variable `x` end;julia> x1
Furthermore,if there is a local variable, e.g., x
,in an outer local scope,assignment to x
in an inner local scopewill 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 end55
In particular,it will not create a new local named x
unless x
is explicitly declared localin the inner scope.This is called shadowing,where names can be reusedto 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 = 3x = 2julia> @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
blockseach introduce a soft local scope.
Soft local scope is the same as hard local scopeexcept 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,nested
for
loops within the REPLwould satisfy this condition,while afor
loop in a functionwould 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 documentationfor the rationale.)
Summary
In this post,we learned how to create and use modules in Juliafor organizing code.We also learned about Julia’s scoping rulesfor global, hard local, and soft local scopes.
How do you use modules in your code?Let us know in the comments below!
Have a better feel for howmodules and variable scope work?Move on to thenext post to learn how to learn new Julia packages!Or,feel free to take a lookat our other Julia tutorial posts.
Additional Links
- Julia Documentation: Modules
- Additional information about modules.
- Julia Documentation: Variable Scope
- Additional information about variable scope.