skip to content

In Ruby, how do the x if y and x unless y statement modifiers work, and when should you prefer them over a full if block?

level: juniorimportance: must knowfreq 58%

answer

  1. condition written last, evaluated first
  2. binds looser than assignment
  3. statement, not an expression
  4. one-line body, simple condition
  5. Style/IfUnlessModifier and line length

basics

~20 s

A modifier puts the condition after the statement: Ruby evaluates the condition first and runs the statement only if it is truthy (if) or falsy (unless). Use it for a one-line body with a short condition, especially guard clauses.

solid answer

~50 s

`x if y` and `x unless y` are statement modifiers: the condition on the right is evaluated first, and the statement on the left runs only when the condition is truthy (`if`) or falsy (`unless`); when it does not run, the result is `nil`. Modifier-if binds more loosely than `=` (and than `and`/`or`), so `total = base + fee if paid` is `(total = base + fee) if paid` — when `paid` is falsy the assignment is skipped entirely. A modifier is a statement, not an expression, so `puts(1 if true)` is a `SyntaxError` unless you wrap it: `puts((1 if true))`. Prefer the modifier for a single short statement, above all guard clauses like `return false unless permit`; use the full block for multi-line bodies, long conditions, or when the condition is the important part. RuboCop's `Style/IfUnlessModifier` pushes one-line bodies into modifier form as long as the line fits `Layout/LineLength`.

code

ruby · 10 lines
ruby
total = 10
paid = false
total = 99 if paid       # parsed as (total = 99) if paid
p total                  # => 10, assignment skipped

result = ("ok" if paid)
p result                 # => nil

# puts( 1 if true )      # SyntaxError: modifier is not an expression
puts((1 if true))        # prints 1

go deeper

for a junior

Explain that x if y runs x only when y is truthy and x unless y only when y is falsy, and give a guard clause such as return false unless permit as the typical use.

for a middle

Explain the precedence: the modifier wraps the whole statement, so an assignment is skipped rather than set to nil. Mention that a modifier is a statement, which is why puts(1 if true) fails to parse.

for a senior

Argue for guard clauses to flatten nested conditionals, and say where modifiers hurt readability: long conditions, multi-line bodies, stacked modifiers. Know which RuboCop cops enforce this and that line length drives Style/IfUnlessModifier.

for a principal

Treat modifier style as a team convention with a real cost: Style/IfUnlessModifier and Style/GuardClause are among the contested cops, so decide deliberately whether to enforce them or leave the choice to review.

## What a statement modifier is Ruby lets `if` and `unless` trail a statement as a **modifier**: ```ruby charge_fee(permit) if permit.expired? send_reminder(permit) unless permit.auto_renew? return false unless permit ``` The left-hand side is the "then" statement, the right-hand side is the **test**. Although the test is written last, it is **evaluated first**; the left side runs only when the test passes (`if`: truthy, `unless`: falsy). When the statement is skipped, the modifier evaluates to `nil`. ## Precedence: the modifier wraps the whole statement The precedence table in Ruby's language reference lists `modifier-if` and `modifier-unless` at the bottom of the operators — below `=`, below `not`, below `and`/`or`; only block braces are listed lower. So the modifier applies to **everything on its left**: | Written | Parsed as | When the test is falsy | |---|---|---| | `total = base + fee if paid` | `(total = base + fee) if paid` | assignment skipped; `total` keeps its previous value | | `log.info("x") unless quiet` | `(log.info("x")) unless quiet` | call skipped | | `a += 1 if a.zero?` | `(a += 1) if a.zero?` | `a` unchanged | A common misreading is that the first line "assigns `nil` when unpaid". It does not assign at all; `total` keeps whatever it held, and it is `nil` only if nothing had assigned it before (the parser still declared it as a local). ## A modifier is a statement, not an expression Ruby's grammar distinguishes statements from expressions. The block form `if c; x; end` is an expression; the **modifier form is only a statement**. Places that require an expression — such as a parenthesised method argument — reject it: ```ruby puts( 1 if true ) # SyntaxError puts((1 if true)) # prints 1: extra parentheses make it an expression x = (1 if true) # parentheses also work on the right of = ``` The language reference documents exactly these three lines. In practice you rarely hit this, because modifiers are meant to stand on their own line. ## When to prefer the modifier Use the modifier form when: - the body is **one short statement** — a call, an assignment, a `return`, `raise`, `next` or `break`; - the condition is **short and secondary** — the reader cares mainly about the action; - you are writing a **guard clause** at the top of a method to bail out early. ```ruby def valid_permit?(permit) return false unless permit return false if permit.revoked? permit.expires_at > Time.now end ``` Guard clauses keep the "happy path" unindented, which is why many Ruby codebases favour them over nested `if` blocks. ## When to use the full block instead 1. **Multi-line bodies.** A modifier hanging off a `begin ... end` or a multi-line call hides the condition below the body; RuboCop's `Style/MultilineIfModifier` flags that. 2. **Long or compound conditions.** When the condition is the important part, lead with it: `if permit.zone == "A" && permit.resident?` is easier to scan than a long line with the test at the far end. 3. **An `else` is needed.** Modifiers have no `else`; if both outcomes matter, write the block (or a ternary for two short values). 4. **Modifier on a modifier.** `x if a unless b` is legal but confusing; `Style/IfUnlessModifierOfIfUnless` rejects it. ## What RuboCop enforces (1.91 defaults) - `Style/IfUnlessModifier` (enabled): flags a block `if`/`unless` whose single-line body would fit as a modifier, and also flags modifier lines that exceed the maximum line length. The length comes from `Layout/LineLength`, whose default `Max` is 120. The config marks it as expected to be disabled by default in the next major release, because the forced shape is contested. - `Style/MultilineIfModifier` (enabled): no modifiers with multi-line bodies. - `Style/IfUnlessModifierOfIfUnless` (enabled): no modifier applied to another conditional. - `Style/GuardClause` (enabled): suggests turning a trailing wrapping conditional into an early `return`/`next` guard. The summary for an interview: the modifier is evaluated condition-first despite being written condition-last, it wraps the whole statement including assignments, it is a statement rather than an expression, and it is the idiomatic choice for short guard-style lines — not for anything with several lines or an `else`.

  • Why is `puts( 1 if true )` a SyntaxError while `puts((1 if true))` prints 1?
    A modifier `if` is a statement, not an expression, and a parenthesised method argument must be an expression. Wrapping the modifier in its own parentheses turns it into an expression, so the doubled parentheses work. Writing `puts (1 if true)` with a space also works, because Ruby then parses the parentheses as grouping the argument rather than as the call's argument list.
  • How do guard clauses built from modifiers change the shape of a method?
    Each precondition becomes an early exit such as `return false unless permit` or `raise ArgumentError, "no zone" if zone.nil?`, so the main logic sits at the method's top indentation level instead of inside nested `if` blocks. RuboCop's `Style/GuardClause` suggests this rewrite when a method body is wrapped in one conditional.
  • Does `x = compute if flag` assign nil to x when flag is false?
    No. Modifier-if has lower precedence than `=`, so the whole assignment is skipped and `x` keeps its previous value. It reads as `nil` only when nothing had assigned `x` before, because the parser already declared `x` as a local variable when it saw the assignment.

saying these in an interview costs you the question

  • A modifier if runs the left-hand statement first and then checks the condition.
  • total = base + fee if paid assigns nil to total when paid is false.
  • A modifier if is an expression, so puts(1 if true) works like the block form.
  • Modifiers are fine for multi-line bodies as long as the condition is short.
  • The unless modifier runs its statement when the condition is truthy.