Skip to content

Instantly share code, notes, and snippets.

@sadish-d
Last active October 18, 2024 13:56
Show Gist options
  • Select an option

  • Save sadish-d/0735a01548cad573d851d08d05b4a38d to your computer and use it in GitHub Desktop.

Select an option

Save sadish-d/0735a01548cad573d851d08d05b4a38d to your computer and use it in GitHub Desktop.
variable scope

variable declaration, assignment, and definition

A variable is a naming symbol used as a placeholder or label for a value. When we declare a variable, we claim a symbol to use as the name for the variable. When we assign a value to a variable, we associate the value with the variable so that we may retrieve the value by referring to the variable’s symbol.

We can declare a variable x in a few explicit or implicit ways (type annotations are optional):

explicit:

  • local x::Int64
  • global x::Int64

implicit:

  • let x::Int64 end
  • f(x::Int64) = nothing
  • function f(x::Int64) = nothing end
  • map([]) do x::Int64 nothing end
  • for x in 0:1
  • (x for x in 0:1)
  • [x for x in 0:1]
  • function x end
  • struct x end
  • x, y = (0, 1)
  • (; x, y) = (x = 0, y = 1)
  • f((x, y)) = x + y

We say a variable is defined when we have declared the variable and assigned a value to it, potentially in a single step. That means a variable is not defined if it is not declared or declared but not assigned to.

module M
    @assert !(@isdefined x) # `x` is not defined (it is not declared)
    global x                # declaring `x` as a global variable
    @assert !(@isdefined x) # `x` is declared but not assigned to, so not defined
    x = 1                   # assigning to `x`
    @assert (@isdefined x)  # `x` is defined
end

In Julia, the expression x = 0 can mean one of two things, depending on the context:

  1. “declare a variable x and assign to it the value 0” (definition. implicit declaration and assignment) or
  2. “assign the value 0 to the variable x that has already been declared” (assignment)

The context that determines which of these meanings applies depends on the scope in which the expression x = 0 gets evaluated. (In some cases, it also depends on whether the expression is evaluated in an interactive environment such as in the REPL as opposed to in a script environment such as in a file. For now, ignore evaluation in interactive environments and consider only evaluation in script environments.)

variable scope

Each variable has a scope, which is the section of code where the value of the variable can be retrieved or where the variable can (potentially) be assigned a value. A variable cannot be accessed outside its scope; so outside a variable’s scope, we can use the variable’s symbol as a name for another variable. Julia has two kinds of scope: local and global.

The following constructs in Julia define the boundaries of local scope:

  1. struct-end
  2. function-end
  3. do-end
  4. let-end
  5. for-end
  6. while-end
  7. try-end
  8. comprehensions
  9. generators

Let's call these the locally-scoped blocks.

The following constructs define the boundaries of global scope:

  1. module-end
  2. baremodule-end

Let's call these the globally-scoped blocks.

If a variable is not declared in a locally-scoped block, its declaration occurs implicitly or explicitly in a globally-scoped block.

A variable that is declared in a locally-scoped block is called a local variable, or simply a local (unless it is explicitly declared global). A variable that is declared in a globally-scoped block is called a global variable, or simply a global (unless it is explicitly declared local).

A locally-scoped block may contain other locally-scoped blocks but not globally-scoped blocks. If a locally-scoped block contains another locally-scoped block, all variables accessible from the outer block are also accessible from the inner one, but local variables declared in the inner block are not accessible from the outer one.

module M
    let                                 # outer locally-scoped block
        let                             # middle locally-scoped block
            x = 0                       # `x` is implicitly declared and assigned to
            let                         # inner locally-scoped block
                @assert (@isdefined x)  # `x` is accessible here
            end
        end
        @assert !(@isdefined x)         # `x` is not accessible here
    end
end

A globally-scoped block may contain locally-scoped or globally-scoped blocks. If a globally-scoped block contains another block, all variables accessible from the outer block are also accessible from the inner one if the inner one is locally-scoped, but none of them are accessible from the inner one if the inner one is globally-scoped.

module Outer                     # outer globally-scoped block
    x = 1                        # `x` is implicitly declared and assigned to
    module Inner                 # inner globally-scoped block
        @assert !(@isdefined x)  # `x` is not acessible
    end
    let                          # inner locally-scoped block
        @assert (@isdefined x)   # `x` is accessible
    end
end

Inside a locally-scoped block, if a variable x is a declared local variable, and the expression x = 0 is in the scope of x (the variable is accessible to the expression), the expression assigns 0 to that variable; otherwise, it declares and assigns to (defines) a new local variable x.

module M
    let
        x = 0
        let
            x = 1               # assigns to an already-declared variable `x`
        end
        @assert x == 1          # `x` was assigned to from an inner block
    end

    let
        let
            x = 1               # declares the local variable `x` and assigns to it
        end
        @assert !(@isdefined x) # `x` is not defined
    end

    let
        let
            x = 1              # assignment to a variable `x` that is declared later in the code
        end
        @assert (@isdefined x) # `x` is defined
        @assert x == 1         # `x` is defined
        local x                # variable declaration; appears later in the code than the assignment
    end

    let
        let
            x = 1              # assignment to a variable `x` that is declared later in the code
        end
        @assert (@isdefined x) # `x` is defined
        @assert x == 1         # `x` is defined
        x = 0                  # implicit declaration; appears later in the code than the assignment
    end

    function f()
        function inner()
            @assert !(@isdefined x) # `x` is not defined
            return nothing
        end
        return inner()
    end
    f()

    function g()
        function inner()
            @assert (@isdefined x) # `x` is defined
            return nothing
        end
        x = 0                      # `x` is implicitly declared and assigned to outside of `inner`
        return inner()
    end
    g()
    
    let x = 0                      # `x` is declared and assigned to outside of `inner`
        function inner()
            @assert (@isdefined x) # `x` is defined
            return nothing
        end
        inner()
    end
    
    let
        function inner()
            @assert (@isdefined x) # `x` is defined
            return nothing
        end
        x = 0                      # `x` is implicitly declared and assigned to outside of `inner`
        inner()
    end
    
    f(y) = true
    let x = 0
        function f(y)
            x = y ^ 2              # assigns to the already-declared `x`
            return x
        end
        sort(99:100; by=f)         # it is not necessary that the last call of `f` is on `100`
        @assert x == 99 ^ 2
    end
    @assert f(0)
end

Variables declared as local in globally-scoped blocks can be accessed only in the expression where they are declared.

module M
    local x = 0
    @assert !(@isdefined x)

    begin
        local x = 0
        @assert (@isdefined x)
    end
    @assert !(@isdefined x)

    local x; x = 0; @assert (@isdefined x)
    @assert !(@isdefined x)
end

An explicit global keyword is required to assign to a declared global variable from locally-scoped blocks.

module M1
    x = 0
    let
        x = 1
    end
    @assert x == 0
end

module M2
    x = 0
    let
        global x = 1
    end
    @assert x == 1
end

module M3
    function outer()
        global x = 0
        function inner()
            x = 1         # this `x` is global
        end
        inner()
        return x
    end
    outer()
    @assert x == 1
end

struct or function blocks are evaluated in global scope even if they are defined inside local blocks. That is, structs and functions defined in local blocks are globals.

module M
    let
        struct S
        end
    end
    @assert (@isdefined S)
    S()
end

The following blocks are neither locally-scoped or globally-scoped:

  • if-end
  • begin-end

So whether a variable is declared inside these blocks does not determine the variable's scope. To determine a variable's scope in such cases, consider instead whether these blocks are themselves contained in locally-scoped or globally-scoped blocks.

closures

Variables captured in closures can be assigned (or reassigned) values after a closure is defined:

module M
    f = Ref{Any}()
    let x
    f[] = ()-> x
    x = 1
    end
    @assert f[]() == 1
    
    counter = Ref{Any}()
    let i = 0
    counter[] = ()-> i += 1; i
    end
    @assert counter[]() == 1
    @assert counter[]() == 2
    @assert counter[]() == 3

    offset = 100
    offset_counter = Ref{Any}()
    let i = 0
        offset_counter[] = ()-> i += 1; i
        i = i + offset
    end
    @assert offset_counter[]() == 1 + offset
    @assert offset_counter[]() == 2 + offset
    @assert offset_counter[]() == 3 + offset
end

for loops

In for-end blocks, each iteration declares a new local variable:

module M
    for i in 0:1
        @assert !(@isdefined x)   # the `x` defined in one loop is not defined in subsequent loops
        x = 1
    end

    fs = Vector{Any}(undef, 2)
    for i in 1:2
        fs[i] = ()-> i
    end
    @assert fs[1]() == 1
    @assert fs[2]() == 2

    gs = Vector{Any}(undef, 2)
    for i in 1:2
        gs[i] = ()-> i
        i += 1
    end
    @assert gs[1]() == 2
    @assert gs[2]() == 3
end

soft versus hard scope

Let's call the following locally-scoped blocks softly-scoped blocks (and the remaining, hardly-scoped):

  • for-end
  • try-end
  • while-end
  • struct-end

Say x is a declared global variable in the REPL (Main module). By default, the expression x = 0 is evaluated as if x were a local variable if it appears in the softly-scoped blocks, which may themselves occur recrsively inside a globally-scoped or softly-scoped block. The variable x, in such cases, is said to have soft scope within the block where the x = 0 expression occurs.

global x
for i in 1:1
    x = 0
end
@assert x == 0

for i in 1:1
    for j in 1:1
        x = 1
    end
end
@assert x == 1
global x
for i in 1:1
    if true
        x = 0
    end
end
@assert x == 0

struct A
    if true
        x = 1
    end
end
@assert x == 1

You can turn off soft scope in the REPL by executing the following, at least as of Julia version 1.10.3:

empty!(Base.active_repl_backend.ast_transforms)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment