Mike Slinn

Arbitrary Context Binding

Published 2025-09-05.
Time to read: 4 minutes.

This page is part of the ruby collection.

Ruby ERB expands Ruby expressions embedded in HTML or markup such that the encapsulated expression is replaced by its value. ERB ultimately calls eval, with some wrapping for safety and binding control.

Nugem allows ERB expressions within templates, and creates an internal ArbitraryContextBinding instance to render templates. This allows variable and method references in ERB templates to be resolved when the render method is called.

Nugem uses ERB templates to create every type of text file that a Ruby Gem needs. This includes Ruby code, RSpec tests, markdown files, .gitignore, and more.

ERB templates support expressions that contain:

  1. References to self
  2. Local, instance, and global variable names
  3. Local and instance public method names
  4. Computation using the above

An ERB template consists of a String expression enclosed in single or double quotes, or provides as a here doc.

Bindings

For ERB to work, a mechanism must exist that can look up the value of a variable or obtain a method body from its name. This mechanism is called a binding. In Ruby, a Binding object encapsulates the execution context at a specific point in the code. Binding objects capture a snapshot of their execution environment.

ERB is not the only thing that uses bindings for this purpose; debuggers, the Ruby REPL, and code generators also use bindings.

The Top-Level Binding

Ruby provides a constant called TOPLEVEL_BINDING that returns the binding of the top-level scope. Use it to access the top-level scope from anywhere in the program. Because the top-level scope never contains local variables, TOPLEVEL_BINDING does not contain local variables either.

When Ruby starts, it sets self to an instance of Object. That instance is not bound to a constant or variable named main. The string "main" is just what you see if you print self:

If you are not one of those persnickety people who find technical minutea interesting, please skip to the next section: If a binding is created at the top-level, then binding’s self.to_s returns the string "main". self is an instance of Object. There is no separate main class or module in Ruby; this output just indicates that the binding currently provides a particular execution context. No standalone documentation page for main exists.

Shell
$ irb
irb(main):001> self
=> main 
irb(main):002> self.class => Object

There is no global variable or constant called main. The String "main" is just the default output of to_s for that particular Object instance.

main Methods

When Ruby finishes initializing, main has the mixture of all the public methods of Object, Kernel, and BasicObject.

  • From Kernel: load, puts, print, p, gets, raise, rand, sleep, and require.
  • From Object: object_id, is_a?, class, tap, and public_send.
  • From BasicObject: ==, !=, !, __send__, and equal?.

The full list can be obtained like this:

Shell
$ ruby -e 'puts TOPLEVEL_BINDING.eval("self.methods.sort")'
!
!=
!~
<=>
==
===
__id__
__send__
class
clone
define_singleton_method
display
dup
enum_for
eql?
equal?
extend
freeze
frozen?
hash
inspect
instance_eval
instance_exec
instance_of?
instance_variable_defined?
instance_variable_get
instance_variable_set
instance_variables
is_a?
itself
kind_of?
method
methods
nil?
object_id
private_methods
protected_methods
public_method
public_methods
public_send
remove_instance_variable
respond_to?
send
singleton_class
singleton_method
singleton_methods
tap
then
to_enum
to_s
yield_self 

Leaky Top-Level Definitions

The top-level binding context is the outermost scope, and is not encapsulated within any class or module.

The hello method shown below is defined at the top-level binding context / outermost scope. Its method definition exists separately in the top-level binding context, distinct from all other entries.

Ruby code
def hello
  'hi'
end

p main.method(:hello) # => #<s;Method: Object#hello>

As you can see, hello is not shown as a method of main, it is an instance method of Object, available everywhere because every object inherits from Object. Promiscuous top-level definitions such as the leakage from the main instance to the Object definition are said to leak globally.

All top-level definitions leak, including methods, constants, local variables, and instance variables. The precise mechanism varies for each type of definition, but the result is the same: definitions propagating through Object to everything inheriting from Object. To prevent accidental leakage, which would pollute the global namespace, wrap your definitions within a module or class instead of defining them at the top level.

Variable and Method Resolution

Here’s how variable and method resolution works:

  1. If a name matches a local variable in the binding, the value is returned.
  2. Ruby next tries to resolve instance variables and method references on the current self object. Instance variables are only accessible if the binding was created inside an object where they exist. Methods are accessible if the object in the binding responds to them.
  3. Ruby then looks up constants through the normal constant lookup rules.
  4. If no suitable binding is found, ERB falls back to TOPLEVEL_BINDING.

Example ERB

The following simple example shows a template String being interpreted according to the local binding.

Ruby code
 1: require 'date'
 2: require 'erb'
 3: 
 4: name = 'Mike'
 5: age = ((Date.today - Date.civil(1956, 10, 15)) / 365).to_i
 6: template = '<%= name %> is <%= age %> years old.'
 7: 
 8: erb = ERB.new template
 9: puts erb.result binding

The above code contains two ERB expressions in the template on line 6. Both ERB expressions implicitly use the binding to look up the value of the given variable name or method name. When I ran the above, the output was:

Output
Mike is 68 years old.

Purpose of arbitrary_context_binding

It is a good practice to evaluate ERBs in a different portion of a code base than where the computation might be performed. For example, Model-View-Controller architectures are based on this style of programming. However, this means that the variables and methods that the ERB needs to evaluate are defined in different binding contexts than where they are used.

Arbitrary_context_binding to the rescue! It allows you to define a virtual binding context consisting of a collections of objects, modules, and a given binding. Method and variable references are resolved by iterating through the entire virtual binding context until the desired reference is found. ArbitraryContextBinding#render invokes ERB#render with the appropriate binding.

The arbitrary_context_binding RSpec tests show how to call Arbitrary­Context­Binding#­result, which in turn invokes ERB#result with the appropriate binding object.

Here is the GitHub’s online editor for the arbitrary_context_binding project.

Usage

Good Examples

Some of the RSpec tests are shown below as regular code to simplify the examples. This code is provided in the playground directory of the arbitrary_context_binding Git project.

This Ruby source file defines modules and objects that the ERB will reference. Notice that every class and variable is defined within a module; none are top-level definitions.

playground/define_stuff.rb
require_relative '../lib/acb_class'

module TestHelpers
  def self.version = '9.9.9'
  def self.helper  = 'helper called'
  def self.greet(name) = "Hello, #{name}!"

  def self.with_block
    yield 'block arg'
  end
end

module OtherHelpers
  def self.helper = 'other helper'
end

module DefineStuff
  include ArbitraryContextBinding

  repository_class = Struct.new(:user_name)
  project_class    = Struct.new(:title)

  @repository  = repository_class.new('alice')
  @project     = project_class.new('cool app')

  obj1 = Struct.new(:foo).new('foo from obj1')
  obj2 = Struct.new(:bar).new('bar from obj2')
  obj3 = Struct.new(:foo).new('foo from obj3')

  @acb_all = ArbitraryContextBinding.new(
    objects:      [obj1, obj2, obj3],
    modules:      [TestHelpers],
    base_binding: binding
  )

  def self.acb_all
    @acb_all
  end
end

The following Ruby code uses the above definitions to expand a template. Note that DefineStuff.acb_all.render calls ERB#render.

playground/usage_good.rb
require_relative 'define_stuff'

module GoodExample
  template = 'User: <%= @repository.user_name %>, Project: <%= @project.title %>'
  puts DefineStuff.acb_all.render template # Displays 'User: alice, Project: cool app'
end

Bad Example

This is an example of how NOT to write Ruby code:

playground/usage_bad.rb
require_relative 'define_stuff'

# This is an example of defining a top-level object:
acb_all = ArbitraryContextBinding.new(
  objects: [],
  modules: [Blah, TestHelpers]
)

# Do not define top-level objects in Ruby unless you have a license ;)
# Look at usage_good.rb for a positive example

template = 'User: <%= @repository.user_name %>, Project: <%= @project.title %>'
puts acb_all.render template # Displays 'User: alice, Project: cool app'
* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.