skip to content

Scoped Refinements

refine and using change a class only inside the file or module that activates them, unlike a global monkey patch. Interviewers probe the lexical scope and what calls still miss the refinement.

on this pageshow

explore

questions

4

In Ruby, why does a String refinement activated with using in one file raise NoMethodError when a helper from another file calls it?

level: middleimportance: should knowfreq 35%

answer

  1. lexical, not dynamic
  2. the call site decides
  3. definition place travels with the method
  4. reopened class starts unrefined
  5. last using wins on a clash

basics

~20 s

Refinements are lexically scoped: a call sees a refinement only if its source text sits after a using in the same file or body. The helper's call was written in another file, so it looks up plain String and misses.

solid answer

~40 s

Ruby decides whether a refinement applies by where the calling code is written, not by who is running at the moment. `using Slugs` in `app.rb` refines the call sites in `app.rb` after that line; a helper in `slugger.rb` that calls `text.to_slug` was compiled without that activation, so the lookup ignores the refinement and raises `NoMethodError`, even when `app.rb` is the caller. The reverse also holds: a method written in a refined scope keeps the refinement wherever it is called from, so `Article#slug` defined after `using Slugs` works for every caller. Blocks written in the refined file carry it too. Reopening `Article` elsewhere starts unrefined, and when two active refinements define the same method, the most recently activated one wins.

code

ruby · 13 lines
ruby
# slugger.rb - no using in this file
module Slugger
  def self.call(text) = text.to_slug
end

# app.rb
require_relative "slugs"
require_relative "slugger"
using Slugs

"Big News".to_slug                 # => "big-news"
["Big News"].map { it.to_slug }    # => ["big-news"]
Slugger.call("Big News")           # NoMethodError, raised inside slugger.rb

go deeper

for a junior

Remember that a refinement applies where the calling code is written, after using, not wherever it happens to run.

for a middle

Explain why a helper in another file raises NoMethodError while a method defined in the refined file keeps working for every caller.

for a senior

Use the lexical rule to decide where refinements fit: safe inside a library, useless for changing third-party behaviour, and dependent on activation order when they overlap.

for a principal

Weigh refinements against global patches as an organisational rule: lexical scoping protects shared code but spreads using lines that reviewers must track.

## Lexical scope in one sentence A **lexically scoped** feature depends on where code is *written*; a **dynamically scoped** one depends on who is *calling* at run time. Ruby refinements are lexical: whether `text.to_slug` sees the `Slugs` refinement is decided by whether that call's source text follows a `using Slugs` in the same file, class body or module body. ## The classic surprise 1. `slugs.rb` declares `refine String` with `to_slug`. 2. `slugger.rb` defines `Slugger.call(text)`, which returns `text.to_slug`. It has no `using`. 3. `app.rb` requires both files, runs `using Slugs`, and calls `Slugger.call("Hello World")`. 4. Ruby raises `NoMethodError` inside `Slugger.call`, because that call site in `slugger.rb` was never refined. The caller's activation does not flow into the callee. This is deliberate: a library cannot be broken by refinements that its callers happen to activate. ## What does carry the refinement | Code | Refined? | Why | |---|---|---| | a call written after `using` in the same file | yes | the call site is inside the activated scope | | a method defined after `using`, called from another file | yes | the method's body was written in the activated scope | | a block written after `using`, passed to a method elsewhere | yes | the block's body is written in the activated scope | | a method in another file, called from the refined file | no | its body was written outside the scope | | a later reopening of the same class without its own `using` | no | each class body activates separately | | code in a file loaded with `require` after `using` | no | refinements do not cross file boundaries | The second row is the useful half of the rule: a class can `using Slugs` inside its own body and offer methods such as `Article#slug` that work for every caller, while the rest of the program never sees `String#to_slug`. ## Precedence when refinements overlap - **Several refinements of the same class**: the refinements active at a call site are searched in reverse order of activation, so the most recent `using` wins. - **Refinements versus the class hierarchy**: lookup checks, for each class in turn, its refinements, then its prepended modules, the class itself and its included modules, before moving to the superclass. A method defined in a **subclass** therefore beats a refinement of a **superclass**: refining `Numeric#/` does not change `1 / 2`, because `Integer#/` is found first. A refinement of `Numeric` that adds a method `Integer` lacks does apply to integers. - **`super` in a refined method** calls the refined class's original method, even if another refinement of it is active in the same scope. ## Why Ruby chose lexical scope Dynamic scoping would let any caller change how code it calls behaves. A library method that calls `to_s` could return something different depending on which application file happened to call it — exactly the global-patch problem refinements were designed to avoid. With lexical scope, the author of each file controls which refinements that file's code sees, and a reader can tell by looking at the `using` lines above the code. The cost is the surprise in this question: refinements do not flow into helpers, so a helper that depends on a refined method must activate the refinement itself. ## Consequences for design - Refinements cannot change how third-party code behaves: a gem that calls `to_s` internally keeps calling the unrefined method. - Helpers that rely on refined methods must activate the refinement in their own file. - Methods that a library defines and later calls on your objects run in the library's scope, not yours. - A refinement never leaks into other files, which is exactly why it is safe to use in a library. ## Debugging checklist - Find the file and body where the failing call is *written*, not the caller. - Check that a `using` for the right module appears above it in that scope. - If the call lives in a reopened class, add `using` to that body as well. - If behaviour differs between two refinements, check which `using` ran last.

  • In Ruby, if a refinement of Numeric defines /, what does 1 / 2 call?
    `Integer#/`. Lookup handles one class at a time: for `Integer` it checks Integer's refinements, prepends, the class and its includes, and finds `Integer#/` before it ever reaches `Numeric` and its refinements. A refined `Numeric` method that `Integer` does not define would still be used for integers.
  • In Ruby, two modules refine String#to_slug differently and one file calls using on both; which runs?
    The one activated last. Active refinements are searched in reverse order of activation, so the later `using` wins for call sites after it. Inside that refined method, `super` goes to the original `String` method, not to the other refinement.

A dialect learned in one classroom: students taught there keep using the new words wherever they later go, but a student from another classroom never learned them, no matter who asks the question.

saying these in an interview costs you the question

  • Believes the caller's using makes the refinement visible inside called methods.
  • Thinks a method defined in a refined scope loses the refinement when called elsewhere.
  • Expects a reopened class body to inherit an earlier body's using.
  • Assumes refinements can change how a gem's internal calls behave.
  • Says a refinement of Numeric#/ changes the result of 1 / 2.
open as a page

In Ruby, how do refine and using add a String#to_slug method without patching String for the whole program?

level: juniorimportance: nice to knowfreq 25%

basics

~20 s

Define the method inside refine String do ... end in a module, then call using with that module. Only code after that using line sees it, up to the end of the file or the class or module body.

open as a page

In Ruby 4.0, which dynamic calls such as send, public_send, method and &:sym honor an active refinement, and why does String.instance_methods not list it?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Written in a scope after using, send, send, public_send, method, instance_method and &:to_slug all honor the refinement in Ruby 4.0. Listings such as methods and instance_methods ignore refinements, so a refined-only method never appears in them.

open as a page

In Ruby 4.0, why must a refine block reuse a helper module's methods through import_methods rather than include, and what can import_methods not copy?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

Inside refine, self is a Refinement whose include and prepend now raise TypeError. Refinement#import_methods, added in Ruby 3.1, copies a module's Ruby-defined methods into the refinement; C-implemented methods raise ArgumentError, and the module's ancestors are skipped with a warning.

open as a page