In a Ruby temperature-conversion script, why can't def to_fahrenheit(celsius) read a factor variable assigned at the top level?
answer
- def starts an empty local table
- class and module do the same
- a block is not a gate
- bare name becomes a method call
- pass it in or use a constant
basics
~20 sdef, 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 linesfactor = 1.8
offset = 32
def to_fahrenheit(celsius)
celsius * factor + offset
end
to_fahrenheit(100)
# NameError: undefined local variable or method 'factor' for maingo deeper
Remember that def, class and module start with no locals from outside, while blocks can see the locals around them.
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.
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.
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