skip to content

In a Ruby temperature-conversion script, why can't def to_fahrenheit(celsius) read a factor variable assigned at the top level?

level: middleimportance: must knowfreq 62%

answer

  1. def starts an empty local table
  2. class and module do the same
  3. a block is not a gate
  4. bare name becomes a method call
  5. pass it in or use a constant

basics

~20 s

def, class and module are scope gates: each starts a new, empty set of local variables, so a top-level local is invisible inside the method and the bare name raises NameError. Pass the value as an argument or make it a constant.

solid answer

~40 s

`def`, `class` and `module` are **scope gates**: each opens a brand-new local-variable table, so a method body sees only its own parameters and the locals it assigns. Inside `to_fahrenheit`, `factor` has never been assigned, so Ruby compiles it as a call to a method named `factor`; none exists, and calling the method raises `NameError` (`undefined local variable or method 'factor' for main`). Blocks are not gates, which is why the same `factor` works inside `readings.each { ... }`. The idiomatic fixes are to pass the value in as an argument or to make it a constant such as `FAHRENHEIT_FACTOR`, because top-level constants are defined on `Object`, which ordinary methods can see. A `$factor` global also works but couples every file to one mutable name.

code

ruby · 9 lines
ruby
factor = 1.8
offset = 32

def to_fahrenheit(celsius)
  celsius * factor + offset
end

to_fahrenheit(100)
# NameError: undefined local variable or method 'factor' for main

go deeper

for a junior

Remember that def, class and module start with no locals from outside, while blocks can see the locals around them.

for a middle

Explain the NameError text: an unassigned bare name is compiled as a method call. Compare gates with blocks and with if and while, which create no scope.

for a senior

Choose the fix on design grounds: explicit arguments or constants over globals, and spot why @factor on main breaks once the method runs on another object.

for a principal

Relate scope gates to code review: methods that pull data from hidden context are harder to test and move, so make explicit parameters the team default.

## The failing script A short conversion script assigns `factor = 1.8` and `offset = 32` at the top level, then defines `to_fahrenheit(celsius)` that uses both. Calling the method raises `NameError`, even though `factor` sits a few lines above it in the same file. Nothing is wrong with the value; the method simply cannot see it. ## Scope gates: def, class and module A **local variable** in Ruby lives in a local table that belongs to one scope. Three keywords open a new, empty table and do not look outward: - **`def`** starts a method body whose only locals at entry are its parameters. - **`class Name`** and **`module Name`** start a body with its own locals, and so does `class << self`. - The **top-level script** is itself a scope; its locals are visible to top-level code and to the blocks written there. These keywords are called **scope gates** because nothing local passes through them. Other constructs behave differently: | Construct | Opens a new local scope? | Sees the enclosing locals? | |---|---|---| | `def` | yes | no | | `class` / `module` body | yes | no | | block (`do ... end` or `{ ... }`) | yes, for locals first assigned inside it | yes | | `if`, `unless`, `while`, `until`, `case`, `begin` | no | it is the same scope | The design keeps a method self-contained: it can be called from anywhere, so it must not depend on whichever locals happened to exist where it was written. ## Why the error names a method When the parser meets a bare lowercase name that has not been assigned earlier in the current scope, it cannot be a local, so Ruby compiles it as a method call with no receiver and no arguments. At run time no method `factor` exists, and Ruby raises `NameError`: ``` undefined local variable or method 'factor' for main ``` - `main` is the top-level object: a `def` at the top level defines a private method on `Object`, and calling it from the script runs it with `self` set to `main`. - If the code had written `factor()` with parentheses, the call would raise `NoMethodError` instead, because the name could only be a method. - Ruby 3.4 changed the quoting in these messages from a backtick to a single quote, so older logs show `` `factor' ``. - Run as a file with `ruby -w`, the script may also warn `assigned but unused variable - factor`, a strong hint that the top-level local and the name inside the method are different things. ## Fixes, from most to least idiomatic 1. **Pass it in.** `def to_fahrenheit(celsius, factor)` makes the dependency explicit and testable. 2. **Make it a constant.** `FAHRENHEIT_FACTOR = 1.8` at the top level is defined on `Object`, and ordinary methods can see constants defined there, so the method reads it without any parameter. Reassigning it later only warns, so it states intent rather than enforcing it. 3. **Use a `$factor` global.** Globals ignore scope gates, so this works, but any code anywhere can change the value. 4. **Avoid `@factor` at the top level.** It sets an instance variable on `main`. The method sees it only while `self` is `main`; called from inside another object, it reads `nil`, and `celsius * nil` raises `TypeError`. Building a method from a block with `define_method`, or keeping the formula in a lambda, also works because blocks capture their surroundings; that closure mechanism is a separate topic. ## Diagnosing it quickly 1. Read the message: `undefined local variable or method` means a bare name was compiled as a method call. 2. Find where the name is assigned and check whether a `def`, `class` or `module` keyword sits between that line and the failing read. 3. Print `local_variables` inside the method: it lists only the parameters and the locals assigned inside, which confirms the gate. 4. Decide whether the value is input (make it a parameter) or configuration (make it a constant), and change the code accordingly. ## What interviewers listen for - The term **scope gate** and the three keywords that create one. - The contrast with blocks, and with `if`/`while`, which create no scope at all. - An explanation of the `NameError` text: the bare name became a method call. - A fix that passes data explicitly instead of reaching for a global.

  • Does class << self also act as a scope gate?
    Yes. `class << self` opens the body of the singleton class, and like any `class` body it starts with an empty local table. A local assigned just before it is invisible inside, and a local assigned inside it is gone once the body ends. Constants and globals still resolve as usual.
  • Why might ruby -w print 'assigned but unused variable - factor' for this script?
    The parser sees the top-level `factor` assigned but never read at the top level; the `factor` inside the method is a different name that it compiled as a method call. Prism, Ruby's default parser since 3.4, emits this verbose warning for files, not for `ruby -e` one-liners, and skips names that start with an underscore. It is a useful clue that a scope gate is involved.
  • Why is @factor at the top level a fragile fix?
    A top-level `@factor` is an instance variable of `main`. The method sees it only while it runs with `self` as `main`, which happens when the script calls it directly. Called from inside another object's method, `self` is that object, `@factor` reads `nil`, and `celsius * nil` raises `TypeError`. Passing the value in avoids that hidden dependency.

saying these in an interview costs you the question

  • Top-level locals are globals, so any method in the file can read them
  • A block starts a scope gate just like def does
  • if and while bodies get their own local scope in Ruby
  • The NameError means factor was garbage-collected before the call
  • Assigning @factor at the top level makes it visible in every method