skip to content

In Ruby 4.0, how do anonymous `*`, `**` and `&` parameters and `...` forward a logging helper's arguments, and what can each form not do?

level: seniorimportance: should knowfreq 35%

answer

  1. ... since 2.7, leading args 3.0
  2. & since 3.1, * and ** since 3.2
  3. no names to read
  4. ... cannot be split into * ** &
  5. no explicit block with (...)

basics

~20 s

Anonymous *, ** and & pass positional arguments, keywords or the block on without naming them; ... forwards all three together. Anonymous parameters cannot be read, and ... cannot be split or joined by a literal block.

solid answer

~50 s

**Anonymous parameters** declare a group without naming it, so the only thing the body can do is pass it on: `*` for positional arguments (Ruby 3.2), `**` for keywords (3.2) and `&` for the block (3.1), as in `def log(*, **, &) = backend.write(*, **, &)`. **`...`** (2.7, with leading arguments since 3.0) forwards positional arguments, keywords and the block together: `def warn(...) = log(:warn, ...)`. The limits: an anonymous group cannot be inspected, so a wrapper that must read or change arguments needs names; `...` cannot be split into `*`, `**` or `&` inside the method; a call with `...` cannot also take a literal block; `...` cannot follow a rest parameter; and a block that declares its own anonymous parameter of the same kind cannot forward the method's. `puts ...` without parentheses parses as an endless range.

code

ruby · 13 lines
ruby
class AuditLog
  def initialize(backend) = @backend = backend

  def warn(...) = log(:warn, ...)

  def log(level, *, **, &)
    @backend.write(level, *, **, &)
  end

  def redacted(level, *messages)
    log(level, *messages.map { |m| m.gsub(/\d{4,}/, "****") })
  end
end

go deeper

for a junior

Recall that ... passes along everything a method received and that a bare * passes along positional arguments without naming them.

for a middle

Explain which version added each form, what each forwards, and the syntax errors for splitting ..., adding a block, or putting ... after a rest parameter.

for a senior

Choose ... for pure delegation, anonymous groups for partial forwarding and names when arguments must be inspected, and roll out Style/ArgumentsForwarding without hiding logic.

for a principal

Decide how far delegation should rely on forwarding syntax versus explicit signatures, weighing less boilerplate against weaker documentation of what a wrapper accepts.

## Why forwarding syntax exists Wrappers, decorators and delegating helpers often need to take whatever they were given and pass it on unchanged. Writing `def warn(*args, **kwargs, &block) = log(:warn, *args, **kwargs, &block)` works, but it names things the method never uses, and hand-written forwarding is where arguments get dropped. Ruby offers two shorter forms. ## The forms and when they arrived | Form | Forwards | Added in | |---|---|---| | `def m(...) = other(...)` | positional args, keywords and the block | 2.7 | | `def m(a, ...) = other(:x, ...)` | leading arguments before `...` | 3.0 | | `def m(&) = other(&)` | the block | 3.1 | | `def m(*) = other(*)` | positional args | 3.2 | | `def m(**) = other(**)` | keywords | 3.2 | The logging helper scenario, forwarding to a backend object: ```ruby class AuditLog def initialize(backend) = @backend = backend def log(level, *) = @backend.write(level, *) def warn(...) = log(:warn, ...) def each_entry(&) = @backend.entries.each(&) end ``` - `log` names `level` because it uses it, and forwards the remaining messages anonymously. - `warn` adds a leading argument and passes everything else, keywords and a block included. - `each_entry` hands its block straight to another method without naming it. ## What each form cannot do 1. **Anonymous groups cannot be read.** Inside `def log(level, *)`, there is no variable for the messages; counting, filtering or redacting them needs a name, `*messages`. 2. **`...` is all or nothing.** In a method declared with `(...)`, `other(*)`, `other(**)` or `other(&)` is a `SyntaxError` (no anonymous rest, keyword rest or block parameter). Use `...` or declare `*, **, &` instead. 3. **No literal block alongside `...`.** `other(...) { }` is a `SyntaxError`, because `...` already carries the block. 4. **Order limits.** `...` must be the last argument in the call and the last parameter in the definition (`def m(..., a)` fails with unexpected parameter order), and `def m(*rest, ...)` is rejected with "... after rest argument". `yield(...)` is rejected too, while `super(...)` inside a `(...)` method is valid. 5. **Blocks with their own anonymous parameters.** Since Ruby 3.3, a block or lambda inside the method may forward the method's anonymous `*` only if it does not declare an anonymous `*` itself; `def log(*); ->(*) { write(*) }; end` is a `SyntaxError`, while `->() { write(*) }` in the same method is fine. The same applies to `**` and `&`. 6. **Parentheses matter.** Ruby's documentation warns that `puts ...` without parentheses is read as `puts()...`, an endless range, with a warning. ## Migrating a hand-written wrapper An existing `def warn(*args, **opts, &block) = log(:warn, *args, **opts, &block)` can be simplified in steps, re-running the tests after each: 1. Confirm the body only passes the groups on and never reads `args`, `opts` or `block`. 2. Replace the whole signature with `(...)` and the call with `log(:warn, ...)` when everything is forwarded to a single call. 3. Where the method reads one group but only forwards another, keep the name for the one it reads and make the other anonymous, for example `def log(level, *messages, **)`. 4. Check the minimum Ruby version the code must support: `...` needs 2.7 (3.0 for leading arguments), `&` needs 3.1, and anonymous `*` and `**` need 3.2. ## Choosing between them - **`...`** for pure pass-through wrappers where the signature should track the target automatically. It carries keywords correctly without any flag, which the older `*args` plus `ruby2_keywords` approach needed. - **Anonymous `*`, `**`, `&`** when the wrapper names some parameters and forwards only one group, or forwards to several calls. - **Named parameters** when the body must look at, change or log the arguments. RuboCop's pending `Style/ArgumentsForwarding` cop suggests `...` or anonymous forms where named groups are only forwarded, with `UseAnonymousForwarding: true` by default, and the pending `Naming/BlockForwarding` cop prefers anonymous `&` for blocks that are only passed on.

  • In Ruby 4.0, why is `def log(...); write(*); end` a SyntaxError?
    A method declared with `(...)` has no anonymous rest parameter to forward, so `*` has nothing to refer to. Inside such a method, arguments can only be passed on as `...`; to forward only positional arguments, declare `def log(*)` instead.
  • When should a Ruby wrapper name its parameters instead of forwarding them anonymously?
    Whenever the body needs to read, count, validate, redact or log the arguments, or pass different subsets to different calls. Anonymous `*`, `**` and `&` have no variable to inspect, and `...` cannot be split, so naming is the only option there.

saying these in an interview costs you the question

  • Inside a def with (...), you can still forward just * or &.
  • Anonymous * can be read inside the method as an implicit args variable.
  • You can pass a literal block alongside ... to override the forwarded one.
  • ... forwards positional arguments only, dropping keywords.
  • puts ... without parentheses forwards the arguments to puts.