How to correctly define and use global variables in the module in Julia?

In fortran, we know that we can in a module define a global variable (use the private property), so that we can use the subroutines in the module to set or change the values of that variable. That updated variable can be used by other functions or subroutines in the module too.
See below,

module Mod
integer, parameter :: r8=selected_real_kind(15,9)
real(kind=r8), private, save :: var
contains
subroutine f(x)
real(kind=r8) :: x
var = 2.0_r8*x
end subroutine f
end

As we can see, we can call f(x) and set the var in the module to be 2x.

Now in Julia, it seems like below,

module Mod
global var
function f(x::Float64)
global var = 2.0*x
return nothing
end
  1. I mean, in the function, if var is on the left hand side, do I have to specify the key word ‘global’ every time?

  2. I also tried to give the global var a type, like

    global var::Float64

But it gives me an error

syntax: type declarations on global variables are not yet supported

It seems i can only do either just specify nothing, like just

global var

or give it a value while setting its type,

global var=0.0::Float64

Is there better to just give the global var a type before using it?

Thank you very much!

Hi CRquantum! And welcome to the Julia discourse!

Having global variables is not considered the best practice because it makes code difficult to track. The other disadvantage is that unless it has a constant type it will affect your performance. But this is what I think you are trying to solve. If you still want to do this, you can use Ref:

global const var = Ref{Float64}(0.0) # Store it into a `Ref` which it's mutable but has a constant type

function f(x::Float64)
    global var[] = 2.0*x
    return nothing
end

f(2.0)

var[] #prints 4.0

You only need to write global the first time that the variable appears in a given scope and its child scopes.

And it doesn’t accept values of other types:

julia> var[] = "hola"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Float64
Closest candidates are:
  convert(::Type{T}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:250
  convert(::Type{T}, ::AbstractChar) where T<:Number at char.jl:180
  convert(::Type{T}, ::CartesianIndex{1}) where T<:Number at multidimensional.jl:136
8 Likes

Thank you so much, Ref is a great idea! May I ask, so what does the braket [ and ] mean in var’[’ ‘]’?

it’s to access the value of the Ref. think of it as accessing the only element in Ref (in this case, to modify it

Cool! Got it! Thanks!

I suppose you do not need explicit global declaration here at all, since Julia properly determines variable scope. You can read about scopes in Scope of Variables · The Julia Language

I mean

const var = Ref{Float64}(0.0) # Store it into a `Ref` which it's mutable but has a constant type

function f(x::Float64)
    var[] = 2.0*x
    return nothing
end

f(2.0)

var[] # prints 4.0
1 Like

Thank you! It seems it only works for const?
I mean if I simply define var as global, then it seems in the function if there is an a, that a is local,

global a = 100.0::Float64
function f(x)::Float64
a = x^2
return a
end
println("f=",f(6.0)," a=",a)

f=36.0 a=100.0
although a is defined outside f as global, inside f, that a is local.

I also say that you do not need explicit global declaration here at all. (See below.)

A. The global and the two ::Float64’s below are redundant:

module A
global a = 100.0::Float64
function f(x)::Float64
    a = x^2
    return a
end
println("f(6.0) = ", f(6.0), ",  a = ", a)
end;
f(6.0) = 36.0,  a = 100.0

B. The A above is equivalent to the following:

module B
a = 100.0
function f(x)::Float64
    a = x^2
    return a
end
println("f(6.0) = ", f(6.0), ",  a = ", a)
end;
f(6.0) = 36.0,  a = 100.0

B′. Don’t write return-type in general (cf. return-type). B′ is better than B and B is better than A.

module B′
a = 100.0
function f(x)
    a = x^2
    return a
end
println("f(6.0) = ", f(6.0), ",  a = ", a)
end;
f(6.0) = 36.0,  a = 100.0

C. We need global for the immutable global variable a in function.

module C
a = 100.0
function f(x)
    global a = x^2
    return a
end
println("f(6.0) = ", f(6.0), ",  a = ", a)
end;
f(6.0) = 36.0,  a = 36.0

D. We don’t need global for the content a[] of the mutable global variable a:

module D
a = Ref(100.0)
function f(x)
    a[] = x^2
    return a[]
end
println("f(6.0) = ", f(6.0), ",  a[] = ", a[])
end;
f(6.0) = 36.0,  a[] = 36.0

E. The D above is type-unstable. We need const for type stability:

module E
const a = Ref(100.0)
function f(x)
    a[] = x^2
    return a[]
end
println("f(6.0) = ", f(6.0), ",  a[] = ", a[])
end;
f(6.0) = 36.0,  a[] = 36.0

For type (un-)stability, please compare the results of @code_warntype D.f(6.0) and @code_warntype E.f(6.0).

Postscript: A code will be more readable and efficient if the global variables that store the parameters of a problem are passed as arguments to functions everytime. See Problem-Algorithm-Solver pattern.

4 Likes

Thank you very much.
So a quick question, yes I got it, Julia want to maintain type stability by using const with Ref. Then the thing is,

  1. if I define all such variable, say if I have 100 such variables, will that make the module very long? That at least seems requires 100 line in Julia.

  2. If I do 1., then the code seems just very long, and somewhat just like Fortran? Then what is point of using Julia?

I have one more question, if I have a 1D array B whose length is 10, say, I can do

const B = Ref(Array{Float64,1}(undef,10))

Now, in some case, In the module, at first, the length of B is unknown, it needs to set by a function, so how to do that?

Like

const B = Ref(Array{Float64,1}(undef, unknown_length  ))
function setlength(j::Int64)
     set the length of B to j
enddo

How to do the above stuff?

Thank you very much!

See Problem-Algorithm-Solver pattern. You’ll find the answer there.

No matter how many parameters describe the problem, putting them in one variable makes it easier to pass them as arguments to the function everytime.

Kind of late to the party here, but I’d strongly recommend not using global variables and putting your code into functions instead, passing parameters around explicitly to called functions. It’ll save you a mountain of similar debugging & performance pain.

For example, in your current code you’d have a single main() function serving as your main entry point where you define initial parameters etc. as well as include a call to your simulation (passing the local variables from main() to it).

2 Likes

Thank you!
By the way, so like, preventing using globals, I know it can make Julia faster etc,
But on the other hand, for other programming Lang, like Fortran or C, is it also a good idea to prevent using global variables?

Generally speaking yes. Global variables are harder for a compiler to reason about, because on modern concurrent and multithreaded processors, unless explicit care is taken about guarding them, most compilers are either conservative about accessing them by inserting locks (this kills performance) or permit a plethora of data races by not doing so (as in C and as far as I know Fortran). This is what the famous example of naively summing a bunch of numbers on multiple threads is about, where you get the wrong results.

In julia, non-const global variables can change type and value at any time (since julia is a multithreaded language), so the compiler is forced to insert type checks and accesses to global memory when using them.

For const global variables, it’s a little bit different, but it’s likely that const doesn’t mean what you think it means. In julia, const is 1) a declaration that the type of a global variable can’t change (this is enforced) and 2) a promise that you won’t also change its value (not enforced). The compiler is allowed to inline const global into functions, so if you change the values after the fact, you may get faulty results because the global may have been inlined into an earlier smaller function (it’s a little more subtle than that, but that would lead to a deepdive into how julias compiler works).

I think you’re going to be interested in the section on constants in the docs:

https://docs.julialang.org/en/v1/manual/variables-and-scoping/#Constants

4 Likes

Well, it’s not exactly about const or mutable/immutables, it’s about scopes. Process of assignment value to a variable is called binding, and since there can be multiple scopes, there are some rules how binding process should occur.

In your example, when you are writing a = x^2 you are in so called hard-scope (scope of the body function). By the scoping rule, existence of global variable with the same name is ignored and new variable is created (inside function scope) and it is assigned a new value, this way, it shadows global variable. You can override this rule with the keyword global, i.e. the following is working as you intent

a = 100.0

function f(x)
    global a = x^2
    return a
end
println("f=",f(6.0)," a=",a)
# f=36.0 a=36.0

I do not know Fortran, so I can be wrong, but by judging how you are using word global, it is possible, that in Fortran global variable means “whenever you meat assignment to this variable (in any scope) always use global variable” and you set this rule once and for all. If I am correct, then it is different from Julia, where global means in this particular scope if you see word "global" then use global variable instead of local. This is why your initial example have redundant global in global a = 100.0 because there is no ambiguity in this case (a is already global in this scope), so it is just ignored by compiler.

Another important thing is relation between mutable and immutable constructs. You can say that these scope rules contradicts to this example

a = [100.0]
function f(x)
    a[1] = x^2
    return a
end

println("f = ", f(6.0), " a = ", a)
f = [36.0] a = [36.0]

and it looks as if scoping rule is ignored if a is vector (or Ref, it is the same for this discussion).

Well, the thing is, a[1] = x^2 is a lie :-). It may look like an assignment, but it is not. In reality it is just a syntax sugar for special function setindex!, so previous code in reality is the following

a = [100.0]
function f(x)
    setindex!(a, 1, x^2)
    return a
end

As you can see, in this representation, there is no rebinding of the variable a. It’s binding hasn’t changed (in C-like terms you can say that it is still the pointer to the same memory region, it is contents of this region has changed). And since a binding hasn’t changed, in this example there is no ambiguity and global variable is used and updated.

If it’s clear, than you should have no problems to understand why following code returns what it returns

a = [100.0]
function f(x)
    a = [2.0]
    a[1] = x^2
    return a
end

println("f = ", f(6.0), " a = ", a)

and compare it to

a = [100.0]
function f(x)
    global a = [2.0]
    a[1] = x^2
    return a
end

println("f = ", f(6.0), " a = ", a)
1 Like

Thank you very much!
Just one more thing, about global variable.
If I define global variables inside each function, and now if I have 10 global variables and each of them are defined inside the functions.

function f1 (x)
global a1 = x^2
return nothing
end

function f2 (x)
global a2 = x^3
return nothing
end

function f3 (x)
global a3 = x^4
return nothing
end

function f10 (x)
global a10 = x^11
return nothing
end

Now you see all the a1, a2, … a10 are defined inside each functions.

Now, it seems if I forgot some variables are global, I may make some mistakes, right?

Is it better just to clearly define all the global variable in the beginning so that it is easier to remember which are global variables?

  global const a1 = Ref{Float64}()
  global const a2 = Ref{Float64}()
 ...

 function f1  ... end
 function f2 ... end
....

Don’t do that.

You may. You won’t if you don’t do that.

Think of how to structure your program if global variables are disallowed altogether.

3 Likes

Don’t use global variables in the function without passing them as arguments.

In my personal opinion, it is also better to avoid using global constants, especially when using Julia for scientific research. Using constants to fix the types inhibits unpredictable composability with various packages.

For example, if you want to see what happens when you perturb the fundamental constant a, and if you fix the type of a with const a = ~, then you will not be able to apply packages such as MonteCarloMeasurements.jl to a.

Every time you use a constant to fix the type, you are inhibiting yourself from coming up with scientific ideas on how to use Julia.

5 Likes

The thing is, globality or locality of the variable is defined by the scope where it is defined, not by the keyword. If you define variable outside of function body it is global, period. There is no meaning in adding keyword global if you define variable outside of function, because it is already global. Keyword global and local only is a hint to compiler, variable from which scope to use in this particular scope. So, for example following function is perfectly valid

function foo()
    global a = 1
    let
        local a = 2
        return a    
    end
end

julia> println(foo(), ", ", a)
2, 1

Here we say that inside function body scope we should use variable a from the global scope (i.e. outside of function body). But after that we defined new scope with the let command and said that we should use variable a of this local scope. So, global or local is not a property of the variable, it is just a compiler hint, which variable to use in this particular scope.

Regarding your question about how to define global variables, I’ll just agree with other commenters: do not use global variables at all. They are needed only in special cases and in most cases, each program which can be written with the help of global variables, can be written without them without losing performance.

2 Likes

I’d like to add that in the vast majority of cases, restructuring the program to use no globals at all and instead passing arguments around explicitly can lead to increases in performance. If the compiler doesn’t have to insert expensive global lookups (because it’s all on the stack of a function), your code is much more likely to be fast (and race free, in case of multithreaded code, which has become much more common in regular julia code since 1.3).

4 Likes