DEV Community

Cover image for Bindings and Lexical Scope in Ruby
Jeff Kreeftmeijer for AppSignal

Posted on • Originally published at blog.appsignal.com

Bindings and Lexical Scope in Ruby

Happy new year, and welcome back to Ruby Magic! In this winter episode, we'll dive into bindings and scopes. So put on your skis and follow us deep into the woods.

Last time, we looked at closures in Ruby by comparing blocks, procs and lambdas. Aside from the differences between the three types, we touched on what defines a closure.

A closure is a first-class function with an environment. The environment is a mapping to the variables that existed when the closure was created. The closure retains its access to these variables, even if they’re defined in another scope.

We've explored Ruby's equivalent to first-class functions, but we conveniently skipped over environments. In this episode, we’ll look at how that environment works for closures, classes and class instances by examining how Ruby handles lexical scope through its bindings.

Lexical Scope

In programming, scope refers to the bindings available at a specific part of the code. A binding, or name binding, binds a name to a memory reference, like a variable's name to its value. The scope defines what self means, the methods that can be called, and the variables that are available.

Ruby, like most modern programming languages, uses a static scope, often called lexical scope (as opposed to dynamic scope). The current scope is based on the structure of the code and determines the variables available at specific parts of the code. This means that the scope changes when code jumps between methods, blocks and classes—as they can all have different local variables, for example.

def bar
  foo = 1
  foo
end

bar #  => 1

In this method, we create a local variable inside a method and print it to the console. The variable is in scope inside the method, as it’s created there.

foo = 1

def bar
  foo
end

bar # => NameError (undefined local variable or method `foo' for main:Object)

In this example, we create the variable outside the method. When we call the variable inside a method, we get an error, as the variable is out of scope. Local variables are tightly scoped, meaning a method can’t access a variable outside itself unless it’s passed as an argument.

@foo = 1

def bar
  @foo
end

bar #  => 1

While local variables are available locally, instance variables are available to all methods of a class instance.

Inherited Scopes and Proc Bindings

As we've seen in the previous examples, the scope is based on the location in the code. A local variable defined outside a method is not in scope inside the method but can be made available by turning it into an instance variable. Methods can't access local variables defined outside of them because methods have their own scope, with their own bindings.

Procs (including blocks and lambda's, by extension) are different. Whenever a proc is instantiated, a binding is created which inherits references to the local variables in the context the block was created.

foo = 1
Proc.new { foo }.call # => 1

In this example, we set a variable named foo to 1. Internally, the Proc object created on the second line creates a new binding. When calling the proc, we can ask for the value of the variable.

Since the binding is created when the proc is initialized, we can’t create the proc before defining the variable, even if the block isn't called until after the variable is defined.

proc = Proc.new { foo }
foo = 1
proc.call # => NameError (undefined local variable or method `foo' for main:Object)

Calling the proc will produce a NameError as the variable isn’t defined in the proc’s bindings. Thus, any variables accessed in a proc should be defined before the proc is created or passed as an argument.

foo = 1
proc = Proc.new { foo }
foo = 2
proc.call # => 2

We can, however, change the variable after it has been defined in the main context since the proc’s binding holds a reference to it instead of copying it.

foo = 1
Proc.new { foo = 2 }.call
foo #=> 2

In this example, we can see that the foo variable points to the same object when in the proc as outside of it. We can update it inside the proc to have the variable outside of it updated as well.

Bindings

To keep track of the current scope, Ruby uses bindings, which encapsulate the execution context at each position in the code. The binding method returns a Binding object which describes the bindings at the current position.

foo = 1
binding.local_variables # => [:foo]

The binding object has a method named #local_variables which returns the names of all local variables available in the current scope.

foo = 1
binding.eval("foo") # => 1

Code can be evaluated on the binding by using the #eval method. The example above isn't very useful, as simply calling foo would have the same result. However, since a binding is an object that can be passed around, it can be used for some more interesting things. Let's look at an example.

A Real-Life Example

Now that we've learned about bindings in the safety of our garage, like take them out on to the slopes and play around in the snow. Aside from Ruby's internal use of bindings throughout the language, there are some situations where binding objects are used explicitly. A good example is ERB—Ruby's templating system.

require 'erb'

x = 1

def y
  2
end

template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"

In this example, we create a variable named x, a method called y, and an ERB template that references both. We then pass the current binding to ERB#result, which evaluates the ERB tags in the template and returns a string with the variables filled in.

Under the hood, ERB uses Binding#eval to evaluate each ERB tag's contents in the scope of the passed binding. A simplified implementation that works for the example above could look like this:

class DiyErb
  def initialize(template)
    @template = template
  end

  def result(binding)
    @template.gsub(/<%=(.+?)%>/) do
      binding.eval($1)
    end
  end
end

x = 1

def y
  2
end

template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"

The DiyErb class takes a template string on initialization. Its #result method finds all ERB tags and replaces them with the result of evaluating their contents. To do that, it calls Binding#eval on the passed binding, with the contents of the ERB tags.

By passing the current binding when calling the #result method, the eval calls can access the variables defined outside of the method, and even outside of the class, without having to pass them explicitly.

Did we lose you in the woods?

We hope you enjoyed our ski trip into the woods. We went deeper into scopes and closures, after glossing over them. We hope we haven't lost you in the woods. Please let us know if you'd like to learn more about bindings, or have any other Ruby topic you'd like to dive into.

Thanks for following us and please kick the snow off your bindings before leaving them for the next developer. If you like these magical trips, you might like to subscribe to Ruby Magic to receive an e-mail when we publish a new article about once a month.

Top comments (0)