In Ruby 4.0, how do anonymous `*`, `**` and `&` parameters and `...` forward a logging helper's arguments, and what can each form not do?
answer
- ... since 2.7, leading args 3.0
- & since 3.1, * and ** since 3.2
- no names to read
- ... cannot be split into * ** &
- no explicit block with (...)
basics
~20 sAnonymous *, ** 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 linesclass 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
endgo deeper
Recall that ... passes along everything a method received and that a bare * passes along positional arguments without naming them.
Explain which version added each form, what each forwards, and the syntax errors for splitting ..., adding a block, or putting ... after a rest parameter.
Choose ... for pure delegation, anonymous groups for partial forwarding and names when arguments must be inspected, and roll out Style/ArgumentsForwarding without hiding logic.
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.