skip to content

In Ruby, when is a default such as `def log(msg, at = Time.now, tags = [])` evaluated, and may it use earlier parameters?

level: middleimportance: should knowfreq 45%

answer

  1. evaluated on every call
  2. only when the argument is omitted
  3. earlier parameters are in scope
  4. explicit nil is a real value
  5. a new [] each call

basics

~20 s

Ruby evaluates a default expression on each call that omits that argument, left to right, so every call gets a fresh Time.now and a new []. A default may use earlier parameters, but passing nil explicitly does not trigger it.

solid answer

~40 s

A Ruby default is an **expression evaluated at call time**, each time the caller omits that argument, and parameters are processed **left to right**. So `at = Time.now` stamps each call separately and `tags = []` builds a new Array per call, which means appending to it never leaks between calls. Unlike Python, whose defaults are evaluated once when the function is defined, Ruby has no shared-mutable-default trap. A default can use **earlier parameters**, instance variables and methods of `self`: `def excerpt(text, limit = text.size / 2)`. Referring to a later parameter fails with `NameError` unless a method of that name exists. Passing `nil` explicitly is a value, not an omission, so `log("boot", nil)` leaves `at` as `nil`.

code

ruby · 13 lines
ruby
def tag(msg, tags = [])
  tags << msg
end

tag(1)            # => [1]
tag(2)            # => [2]  (a new Array each call)

def excerpt(text, limit = text.size / 2)
  text[0, limit]
end

excerpt("disk full")        # => "disk"
excerpt("disk full", nil)   # TypeError: no implicit conversion from nil to integer

go deeper

for a junior

Recall that defaults are used only when the argument is omitted and are evaluated on every such call.

for a middle

Explain left-to-right evaluation, defaults that use earlier parameters, the NameError for later ones, and why an explicit nil bypasses the default.

for a senior

Watch for costly or side-effecting defaults and nil-versus-omitted ambiguity in shared APIs, and use a sentinel or an explicit nil contract where it matters.

for a principal

Define how public methods treat nil versus omitted arguments so the contract is consistent across libraries and does not depend on each author's habits.

## Defaults are expressions run per call A **default value** in Ruby is not a stored constant: it is an expression the method runs at **call time**, and only when the caller did not supply that argument. Parameters are handled **left to right**, so each default can see what came before it. ```ruby class AuditLog def log(msg, at = Time.now, tags = []) tags << :audit "#{at.strftime('%H:%M:%S')} #{msg} #{tags.inspect}" end end audit = AuditLog.new audit.log("boot") # a fresh time and tags = [:audit] audit.log("ready") # a new time again, tags = [:audit] again ``` Consequences of per-call evaluation: - **Time-based defaults are current.** `at = Time.now` records when each call happened, not when the class was loaded. - **Mutable defaults are fresh.** `tags = []` allocates a new Array for every call that omits it, so `tags << :audit` never accumulates across calls. - **Defaults can be expensive.** A default that queries a database or reads a file runs every time the argument is omitted; move such work into the body behind a condition if it is not always needed. ## The contrast interviewers ask about Python evaluates default expressions **once**, when the function is defined, and every call that omits the argument shares that single object. That is why a Python function with a list default can accumulate values across calls. Ruby's model is the opposite, so the equivalent Ruby method returns `[1]` for the first call and `[2]` for the second, not `[1, 2]`. | Question | Ruby | |---|---| | When is the default evaluated? | on each call that omits the argument | | Is a `[]` default shared between calls? | no, each call gets a new Array | | Can it reference an earlier parameter? | yes, left to right | | Can it reference a later parameter? | no, `NameError` unless a method has that name | | Does an explicit `nil` trigger it? | no, `nil` is bound as passed | ## Using earlier parameters, and the limits Because parameters bind in order, a default can depend on anything bound before it: 1. **Earlier parameters:** `def excerpt(text, limit = text.size / 2)`. 2. **Chains of defaults:** `def box(width, height = width, depth = height)`. 3. **Instance state and methods:** `def log(msg, level = default_level)`, where `default_level` is a method of the same object. The reverse direction does not work. In `def span(a = b, b = 1)`, when `a`'s default runs, `b` is not yet a local variable, so Ruby treats `b` as a method call and raises `NameError` for an undefined local variable or method, unless the object happens to have a method called `b`, which is worse because it silently calls it. ## What a default may contain A default is any Ruby expression, not only a literal: - **Method calls:** `at = Time.now`, `level = default_level`. - **Conditionals:** `limit = text.size > 80 ? 40 : text.size`. - **Raising:** `def fetch(key, fallback = raise(ArgumentError, "fallback required"))` makes an apparently optional parameter fail loudly with a custom message when omitted. - **Constants and frozen values:** `tags = DEFAULT_TAGS` shares one object across calls deliberately, so the method must not mutate it; freezing the constant enforces that. Whatever the expression is, it runs in the method's own scope, with `self` set to the receiver and the earlier parameters already bound. ## Explicit nil is not an omission A default fires only when the **argument is missing**. A caller who passes `nil` has supplied a value: - `audit.log("boot", nil)` binds `at = nil`, and `at.strftime` then raises `NoMethodError`. - If `nil` should mean "use the default", write the parameter as `at = nil` and resolve it in the body with `at ||= Time.now`. - To distinguish "omitted" from "passed nil" exactly, use a private sentinel object as the default, `NOT_GIVEN = Object.new`, and compare with `equal?`. ## Key points - Defaults run at call time, only for omitted arguments, left to right. - Mutable and time-based defaults are fresh per call. - Earlier parameters are visible to later defaults; later ones are not. - `nil` passed explicitly is bound as `nil`.

  • In Ruby, with `def box(width, height = width, depth = height)`, what does `box(3)` bind?
    `width = 3`, `height = 3` and `depth = 3`. Each default runs left to right after the parameters before it are bound, so `height` sees `width` and `depth` sees `height`. `box(3, 5)` gives `depth = 5`.
  • How can a Ruby method tell a caller who omitted an argument from one who passed nil?
    Use a private sentinel as the default, for example `NOT_GIVEN = Object.new` and `def log(msg, at = NOT_GIVEN)`, then check `at.equal?(NOT_GIVEN)`. A `nil` default cannot make the distinction, because both cases arrive as `nil`.

saying these in an interview costs you the question

  • Ruby evaluates a default once, when the method is defined.
  • A tags = [] default is shared by every call, so values pile up.
  • Passing nil explicitly makes Ruby use the default instead.
  • A default can refer to any parameter, including later ones.
  • Defaults must be literals, not method calls or expressions.