Introduction
Ruby has three keywords — class, module, and def — that act as scope gates, creating a fresh local-variable scope and blocking access to outer variables. This lesson explains what scope gates are, how to bypass them with the flat-scope technique, and when you actually want the isolation they provide.
Key Concepts
- Scope Gate: A keyword (
class,module,def) that opens a new local-variable scope, making outer local variables inaccessible. - Flat Scope: A technique that replaces scope-gate keywords with block-based alternatives (
Class.new,define_method,class_eval) to share variables across scopes. - Clean Room: An intentionally isolated scope created with
instance_evalon a fresh object, preventing access to the caller's state.
Real World Context
When writing a plugin system, you might need a shared registry variable that is accessible inside dynamically defined methods. The class and def keywords would block access to that variable. Flat scope lets you use Class.new and define_method instead, keeping the registry visible. Rails initializers and Rake tasks rely on this pattern to share configuration across method definitions.
Deep Dive
Three keywords create new scopes, blocking access to outer local variables:
classmoduledef
The following example demonstrates the scope gate created by class:
rubyouter_var = "I'm outside" class MyClass # outer_var is not accessible here! puts outer_var # NameError! end
The class keyword opens a new scope, so outer_var is invisible inside the class body.
You can flatten the scope by replacing keywords with block-based alternatives that form closures:
rubyouter_var = "I'm outside" # Instead of class: MyClass = Class.new do puts outer_var # Works! "I'm outside" end # Instead of def: MyClass.class_eval do define_method(:greet) do puts outer_var # Works! end end
Class.new with a block and define_method both use closures, so they retain access to outer_var. This is the flat-scope technique.
Here is a practical example where flat scope solves a real problem:
rubyshared_secret = "password123" class Authenticator # Can't access shared_secret with def # def verify(input) # input == shared_secret # NameError! # end end # Use flat scope instead Authenticator.class_eval do define_method(:verify) do |input| input == shared_secret # Works! end end
The class_eval block captures shared_secret, and the define_method block inside it also retains that reference. The method can now access the variable even though it was defined outside the class.
Sometimes you want the opposite — a completely clean scope that prevents the caller from leaking state:
rubymodule DSL def self.run(&block) # New scope, clean environment CleanRoom.new.instance_eval(&block) end end
The CleanRoom object provides a controlled environment where only its own methods are available, preventing accidental access to the caller's variables.
Common Pitfalls
- Overusing flat scope — Sharing variables across many scopes creates hidden coupling. If a method depends on a captured variable, it becomes harder to understand and test in isolation.
- Forgetting that blocks are NOT scope gates —
do...endand{ }blocks,if/unless, andbegin/rescuedo not create new scopes. Variables defined inside them leak out to the surrounding scope.
Best Practices
- Default to scope gates — Use
classanddefnormally. Only reach for flat scope when you have a concrete need to share a variable across boundaries. - Document shared variables — When using flat scope, comment which variables are captured and why, so future readers understand the coupling.
Summary
class,module, anddefare scope gates that start fresh local-variable scopes.- Replace them with
Class.new,Module.new,class_eval, anddefine_methodto flatten scope and share variables. - Use clean rooms when you want intentional isolation rather than accidental leakage.
Code Examples
db_url = ENV.fetch('DATABASE_URL')
# Flat scope: Class.new + define_method share db_url
AppDb = Class.new do
define_method(:connect) do
puts "Connecting to #{db_url}"
end
end
AppDb.new.connect # => "Connecting to postgres://..."