DEV Community

Gernot Gradwohl
Gernot Gradwohl

Posted on • Originally published at Medium

Under Observation — Variable Scoping and How It Affects Your Program

Let’s take a look at taking a look

When someone begins in programming, one of the basic concepts they are introduced to is name resolution — aka, scoping. Scopes are very important because they determine if a variable/method is visible or not. Also, scopes help create a tidy and clean global namespace, because you can “hide” your variables in a subscope.

After learning the basics of programming, scopes still remain very important because they are essential for knowing what is going on in a more advanced concept, metaprogramming. Especially in Ruby, many metaprogramming facilities work by bending the scope code is executed under. For example, the methods class_eval or instance_eval will bend the scope to another object/class.

To grasp that concept better let’s take a closer look at scopes. What type of scopes exist, and what level of scopes are commonly used?

What Is a Scope?

The definition is: A scope is a part of a program.

This sounds very vague. So let’s make it more clear. A scope is the part of the program where a variable is visible and accessible. The part where a variable is visible can be nested in various levels — more on this later.

In computer science, there are two types of scoping: lexical and dynamic. The type depends on the programming language you’re using.

Lexical Scoping

This is the type that all modern languages implement, as far as I know. Lexical scoping means that the context of the source code is the scope. For example:

def test_method
  method_variable = 4
  different_variable = [1,2,3]
  different_variable.map do |number|
    number * method_variable
  end
end
Enter fullscreen mode Exit fullscreen mode

All your variables are defined in the context of your current method. Inside the block, we access the variables from the method which is one level above. But when you read the code it’s very clear what the value of method_variable is because of the context.

Dynamic Scoping

This implementation isn’t used in many languages anymore. Some that are still in use and implement scoping this way are Perl (only if the user opts-in), Bash, Emacs Lisp, and LaTeX.

So, how does dynamic scoping work? The name resolution depends on the runtime. Let’s adjust the example above so that we can see the difference. (Remember, Ruby doesn’t use dynamic scoping, so this is a pseudo-Ruby code and won’t work in your console).

def multiply_by_two
  multiplicand = 2
  test_method
end

def multiply_by_three
  multiplicand = 3
  test_method
end

def test_method
  different_variable = [1,2,3]
  different_variable.map do |number|
    number * multiplicand
  end
end

puts multiply_by_two   # => [2,4,6]
puts multiply_by_three # => [3,6,9]
Enter fullscreen mode Exit fullscreen mode

In a dynamically-scoped language the runtime looks in the current block for the variable, then it looks in the callstack above to search for the variable. In our example, the method multiply_by_two has the variable multiplicand with 2 initialized and the method multiply_by_three with 3. The result of the test_method really depends on the method that it’s calling.

With this example, the disadvantage is very clear. Dynamic scoping voids the benefits of referential transparency since it depends on the call stack/runtime and it is very hard to reason about the value of a variable.

What levels of scoping do we generally have?

Levels of Scope

Global

As the name suggests, global scoping is when a variable or method is visible in the whole application. This is not necessarily bad for small scripts but as your application grows an extensive use of global variables will make your code hard to maintain.

Also, global variables are especially complicated to get right when it comes to multithreading.

Module

In module scoping, the variable is visible inside a module but modules can be structured across multiple files.

File

In file scoping the variable is only visible inside a file. This level is mainly used in C (sometimes in C++ as well). In C it is possible to define a variable in a file but not export it, so other functions can’t use it. If the developer wants to use a file-scoped variable they have to define it at the beginning of the file and this must not be inside a function.

File scoping is very similar to module scoping in the special case that a file is a module.

Function

In function scoping, function is defined in a variable and accessible in any sub-block of that function.

This is the way we used the variable method_variable in our example for lexical scoping.

In JavaScript functions, function scoping is the default scope level for every variable that is initialized with the var keyword (if a variable is initialized without any keyword, it becomes a global variable by default). Why is that the case? Because JavaScript puts any of those variables implicit at the top of the function.

function x() {
   if(true) {
      var x = "Interesting"
   }
   alert(x);
}
Enter fullscreen mode Exit fullscreen mode

Here the variable x is still available although it is defined in a sub-block of the function.

The implicit change of order for the variables with var is used to make some funny code work:

function x() {
    alert(x);
    var x = "Interesting…"
}
Enter fullscreen mode Exit fullscreen mode

Up-to-date Chrome or Firefox doesn’t work like this anymore, but still, a variable defined with var is visible in the whole function not just in a block.

Block

A function can be divided into sub-blocks. In many C-like languages a block is surrounded by {} — in the case of Ruby, we use do end more often.

Expression

In many functional languages, we have an additional level, the expression level. This means that a variable is only defined in a short expression.

This can look like this:

let val x = some_function() in x * x end

Here we are using the variable x as a cache so we don’t have to call the function some_function() twice.

Note: The let keyword exists in non-functional languages as well — e.g. in JavaScript or Rust—but in these languages, it has nothing to do with how the variable is scoped.

Connected Concepts

Dynamic dispatch is very closely connected to scoping. This describes how to resolve instance variables/methods for objects (most of the time).

Closures

Sometimes, closures are confusing when it comes to scoping. Why is that so? Because generally speaking they are executed in a different place than where they are coded.

Nevertheless, they are lexically scoped because the context of the code is relevant for name resolution rather than the runtime.

Conclusion

If we carefully consider the scoping of our code and remain aware of the context it is executed in, we’ll be able to write better code and not become confused when we are using domain-specific languages (which bend scopes) — something quite common in the Ruby community, especially in Rails, but not too uncommon in JavaScript as well.

Top comments (0)