skip to content

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%

answer

  1. the calling scope still decides
  2. send and &:sym since 2.4
  3. public_send since 2.6
  4. method and instance_method since 2.7
  5. listings ignore refinements

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.

solid answer

~40 s

Early refinements ignored every indirect call, and old articles still say so. Ruby added support step by step: `send`, `__send__` and `Symbol#to_proc` (`&:to_slug`) in 2.4, string interpolation in 2.5, `public_send` in 2.6, and `method` and `instance_method` in 2.7. In Ruby 4.0 all of them honor a refinement **when the call itself is written in the refined scope**. `public_send(name)` executed inside a gem is still unrefined, because the gem's call site was written elsewhere. Listing methods ignore refinements: `String.instance_methods`, `"x".methods` and similar return the unrefined view, so a method that exists only in a refinement is missing. Code that decides behaviour by scanning those lists, rather than calling, will not see the refinement; `Module.used_modules` shows what is active in the current scope.

code

ruby · 10 lines
ruby
using Slugs

"Big News".send(:to_slug)          # => "big-news"
"Big News".public_send(:to_slug)   # => "big-news"
"Big News".method(:to_slug).call   # => "big-news"
["Big News"].map(&:to_slug)        # => ["big-news"]

String.instance_methods.include?(:to_slug)  # => false
"Big News".methods.include?(:to_slug)       # => false
Module.used_modules                         # => [Slugs]

go deeper

for a junior

Recall that in current Ruby, send, public_send, method and &:sym written after using all see the refinement, while methods listings do not.

for a middle

Explain that the call site of the indirect mechanism decides, so a send written in another file ignores your refinement even with your symbol.

for a senior

Spot code that detects features by scanning instance_methods or methods, which silently misses refinements, and move the dynamic dispatch into the refined scope.

for a principal

Account for version drift when adopting refinements: guidance written before 2.7 is wrong on indirect calls, and team docs should say which Ruby they assume.

## Direct and indirect calls A **direct call** names the method in source code: `title.to_slug`. An **indirect call** reaches a method through a value computed at run time: `title.send(:to_slug)`, `titles.map(&:to_slug)`, `title.method(:to_slug)`. Because refinements are lexically scoped — they apply to call sites written after `using` in the same file or body — each indirect mechanism needed explicit support to know which scope it was called from. ## The timeline | Mechanism | Honors refinements since | Note | |---|---|---| | `send`, `__send__` | 2.4 | uses the scope where `send` is written | | `&:sym` (`Symbol#to_proc`) | 2.4 | the `&:to_slug` must be written in the refined scope | | `"#{obj}"` interpolation calling `to_s` | 2.5 | a refined `to_s` is used | | passing an object with a refined `to_proc` as `&obj` | 2.6 | block passing | | `public_send` | 2.6 | same rule as `send` | | `method`, `instance_method` | 2.7 | returns the refined method object | All of these hold in Ruby 4.0. The `doc/syntax/refinements.rdoc` page shipped with 4.0 still states that `Kernel#method` does not honor refinements; the 2.7 change log and the implementation (`Kernel#method` resolves through the refinement-aware lookup) show that sentence is out of date. ## The rule behind all of them The deciding scope is **the place where the indirect call is written**, not the place where the method name came from: - `"Big News".send(:to_slug)` in a file that ran `using Slugs` works. - A dispatcher in another file, `obj.public_send(name)`, called with `:to_slug` from the refined file, raises `NoMethodError`: the `public_send` call site is unrefined. - `["Big News"].map(&:to_slug)` in the refined file works, even though `map` itself is implemented elsewhere, because the `&:to_slug` conversion happens at the refined call site. ## What does not see refinements Methods that **list** rather than **call** ignore refinements: - `String.instance_methods`, `String.public_instance_methods` - `obj.methods`, `obj.public_methods` For a method that exists only in a refinement, these lists do not include it; for a refinement that overrides an existing method, they show the original method's entry. Code that inspects these lists to decide what to call — serializers, form builders, some test helpers — will not notice the refinement. The official refinements document says this behaviour "may be changed in the future", so it should not be relied on as a feature either. ## Why listings behave differently A listing asks the class for everything it defines. Ruby answers from the class's method table and resolves each refined entry as if no refinement were active — the implementation passes no scope at all. The result is stable no matter where the listing is requested — the refined file and an unrefined file get the same list — which is predictable, but means listings cannot be used as evidence that a refined method is callable. To test availability in the current scope, call the method, or obtain it with `method(:to_slug)`, which honors refinements. ## Seeing what is active Ruby offers reflection on refinements themselves: 1. `Module.used_modules` returns the modules activated with `using` in the current scope. 2. `Module.used_refinements` (3.2) returns the `Refinement` objects active in the current scope. 3. `Module#refinements` (3.2) returns the refinements a module defines, and `Refinement#target` (3.3) the class each one refines; the older `Refinement#refined_class` was removed in 3.4. ## Practical guidance - Prefer direct calls to refined methods; they are the easiest to reason about. - When dispatch must be dynamic, keep the `send` or `public_send` inside the refined file. - Do not use listings to detect refined methods; call through `method` or check `Module.used_modules` instead. - When reading older blog posts, check the Ruby version: anything written before 2.7 undercounts what honors refinements.

  • In Ruby 4.0, why does ["Big News"].map(&:to_slug) work in a refined file although Array#map is not refined?
    The `&:to_slug` conversion from symbol to block happens at the call site, which is written in the refined file. Since Ruby 2.4 `Symbol#to_proc` created there resolves `to_slug` with that scope's refinements, so `map` receives a block that calls the refined method. Where `map` itself is implemented does not matter.
  • In Ruby 4.0, how can code check which refinements are active at a given point?
    `Module.used_modules` lists the modules activated with `using` in the current scope, and `Module.used_refinements` (added in 3.2) lists the individual `Refinement` objects. `Refinement#target` (3.3) tells which class each refines. Method listings such as `instance_methods` cannot answer this, because they ignore refinements.

saying these in an interview costs you the question

  • Claims send never honors refinements, citing pre-2.4 behaviour.
  • Believes public_send honors a refinement wherever it runs, even in a gem.
  • Uses String.instance_methods to prove a refined method is available.
  • Says &:to_slug cannot reach a refined method because map is defined in C.
  • Trusts the refinements doc page's claim that Kernel#method ignores refinements.