skip to content

In Ruby, what does `def log(*messages)` collect, and how do `log(lines)` and `log(*lines)` differ when lines is an Array?

level: juniorimportance: must knowfreq 70%

answer

  1. always an Array, maybe empty
  2. one Array argument stays nested
  3. splat at the call site
  4. *nil expands to nothing
  5. only one rest parameter

basics

~10 s

*messages gathers every leftover positional argument into a new Array, empty when there are none. log(lines) passes one argument, so messages is [lines]; log(*lines) splats the Array into separate arguments first.

solid answer

~40 s

`def log(*messages)` declares a **rest parameter**: every positional argument not taken by other parameters is collected into a new `Array`, which is `[]` when the caller passes none, never `nil`. The splat at the **call site** is the mirror image. `log(*lines)` expands the Array into separate arguments, so `messages` holds the elements, while `log(lines)` passes one argument and `messages` becomes `[lines]`, a nested Array. A call-site splat works on any value: `nil` expands to no arguments, an object with `to_a` such as a Range is converted, and anything else is passed as a single argument. A method may declare only one rest parameter.

code

ruby · 10 lines
ruby
def log(level, *messages)
  messages
end

lines = ["disk full", "retrying"]
log(:warn, lines)          # => [["disk full", "retrying"]]
log(:warn, *lines)         # => ["disk full", "retrying"]
log(:warn, *nil)           # => []
log(:warn, *(1..3))        # => [1, 2, 3]
log(:warn, *"x")           # => ["x"]

go deeper

for a junior

Recall that *messages is always an Array, possibly empty, and that log(*lines) spreads an Array while log(lines) passes it as one argument.

for a middle

Explain what a call-site splat does with nil, Ranges and plain objects, and where a rest parameter may sit among required, optional and trailing parameters.

for a senior

Choose between variadic and collection parameters for shared helpers, and normalise deliberately so callers cannot hit the nested-Array bug.

for a principal

Set conventions for variadic public APIs across a codebase so helpers are called the same way everywhere and review does not relitigate the style.

## The rest parameter collects leftovers A **rest parameter** is written with a leading asterisk, `*messages`, and it collects every **positional argument** that the method's other positional parameters did not take. It is always a fresh `Array`: - With no leftover arguments it is `[]`, never `nil`, so `messages.each` is always safe. - Its elements are the argument objects themselves, not copies. - A method may declare **only one** rest parameter; `def log(*a, *b)` is a `SyntaxError`. The scenario below is a logging helper that accepts any number of messages after a level: ```ruby class AuditLog def log(level, *messages) messages.each { |m| puts "[#{level}] #{m}" } messages.size end end audit = AuditLog.new audit.log(:info, "boot", "ready") # two lines, returns 2 audit.log(:info) # no output, returns 0 ``` ## The splat at the call site The same asterisk used **in a call** does the opposite job: it expands a collection into separate arguments. This is where the classic bug lives. | Call | `messages` inside `log(level, *messages)` | |---|---| | `audit.log(:warn, "disk full", "retrying")` | `["disk full", "retrying"]` | | `audit.log(:warn, lines)` with `lines = ["disk full", "retrying"]` | `[["disk full", "retrying"]]` | | `audit.log(:warn, *lines)` | `["disk full", "retrying"]` | | `audit.log(:warn, *lines, "done")` | `["disk full", "retrying", "done"]` | | `audit.log(:warn)` | `[]` | Passing `lines` without a splat hands over **one** argument, the Array, so the method sees a single message whose string form is the whole Array. Nothing in Ruby flattens it automatically. ## What a call-site splat does with other values 1. **`nil`** expands to no arguments: `audit.log(:warn, *nil)` leaves `messages` as `[]`. 2. **Objects that respond to `to_a`** are converted first: `*(1..3)` passes `1, 2, 3`. 3. **Anything else** is passed as a single argument: `*"x"` passes `"x"`, because `String` has no `to_a`. 4. A splat can sit anywhere in the argument list and can appear more than once: `log(:info, *before, "middle", *after)`. The splat also fills ordinary positional parameters, not just rest parameters. With `def pair(a, b)`, the call `pair(*[1, 2])` binds `a = 1` and `b = 2`. ## Where the rest parameter can sit - After required and optional parameters: `def log(level, prefix = "app", *messages)`. - Before trailing required parameters: `def wrap(open, *parts, close)` is valid, and the trailing ones are filled first. - Before keyword parameters and a block parameter, which come later in the list. - It may be **anonymous**, a bare `*`, when the method only forwards the arguments on or ignores them. ## The rest Array belongs to the method The Array a rest parameter receives is **new on every call**, even when the caller splatted an existing Array: - `log(:info, *lines)` followed by `messages << "extra"` inside the method leaves the caller's `lines` unchanged. - The **elements** are still the caller's objects, so `messages.first << "!"` would mutate a caller's String if it is not frozen. - Because the rest parameter is a new Array on every call, a very hot method with `*messages` builds one per call; that rarely matters, but it is worth knowing when profiling allocation-heavy code. This independence makes rest parameters safe to reshape inside the method with `shift`, `pop` or `compact!` without surprising the caller. ## Design notes for variadic helpers - Accept `*messages` when callers naturally list values inline; accept one Array parameter when callers already hold a collection. Mixing the two leads to the nested-Array bug above. - If both call styles must work, normalise explicitly with `messages.flatten`, and document it, rather than guessing inside the method. - Keep the rest parameter's name meaningful: `messages` tells readers what the Array holds, while `args` says nothing.

  • In Ruby, what does `def pair(a, b)` receive when called as `pair(*[1, 2])`?
    `a` is `1` and `b` is `2`. A call-site splat expands the Array into two separate positional arguments before binding, so it works with ordinary parameters as well as with rest parameters. `pair([1, 2])` would instead raise `ArgumentError` (given 1, expected 2).
  • How would a Ruby logging helper accept both `log(:info, "a", "b")` and `log(:info, ["a", "b"])` safely?
    Declare `*messages` and normalise with `messages.flatten` before using them, documenting that nested Arrays are flattened. The rest parameter alone would receive `[["a", "b"]]` for the second call, because an unsplatted Array is one argument.
  • In Ruby, can a method declare two rest parameters, such as one for errors and one for warnings?
    No. A parameter list may contain only one rest parameter, and `def log(*errors, *warnings)` is a `SyntaxError`, because Ruby could not decide where one ends. Use two Array parameters or keyword parameters to separate the groups.

saying these in an interview costs you the question

  • *messages is nil when no extra arguments are passed.
  • log(lines) and log(*lines) receive the same messages Array.
  • Ruby flattens an Array passed to a rest parameter automatically.
  • Splatting nil raises an error.
  • A method can have several rest parameters for different groups.