skip to content

In Ruby, what do the trailing ? in overdue? and the trailing ! in renew! promise, and what do they not promise?

level: juniorimportance: must knowfreq 74%

answer

  1. characters in the name, nothing enforced
  2. ? returns truthy or falsy
  3. nonzero? returns self or nil
  4. ! is dangerous relative to its twin
  5. push mutates without a bang

basics

~20 s

Both are conventions Ruby does not enforce. A ? method answers a yes/no question with a truthy or falsy value, not always true or false. A ! method is the more dangerous twin of a same-named method, often mutating or raising.

solid answer

~40 s

`?` and `!` are simply characters allowed at the end of a method name; Ruby enforces nothing about them. By convention a **predicate** such as `overdue?` answers a question with a truthy or falsy value, not always `true` or `false`: `5.nonzero?` returns `5`, `0.nonzero?` returns `nil`, and `File.size?` returns an Integer or `nil`. A **bang** method such as `renew!` marks the *more dangerous* member of a pair. In the core library that usually means it modifies the receiver where the plain method returns a new object (`sort` vs `sort!`), but it can also mean skipping safety steps: `exit!` ends the process without running `at_exit` handlers. `!` does **not** mean "mutates": `Array#push`, `String#concat` and `Hash#delete` all mutate with no bang. The convention is to add `!` only when a non-bang twin exists.

code

ruby · 22 lines
ruby
class LibraryLoan
  MAX_RENEWALS = 2
  RenewalLimitReached = Class.new(StandardError)

  def initialize(due_day)
    @due_day = due_day
    @renewals = 0
  end

  def overdue?(today) = today > @due_day

  def renew
    return false if @renewals >= MAX_RENEWALS
    @renewals += 1
    @due_day += 14
    true
  end

  def renew!
    renew or raise RenewalLimitReached, "limit #{MAX_RENEWALS}"
  end
end

go deeper

for a junior

Know that ? names a question returning truthy or falsy and ! names the dangerous twin, and that neither is enforced by Ruby.

for a middle

Give counterexamples both ways: push and delete mutate without a bang, exit! is dangerous without mutating, and nonzero? returns a number or nil.

for a senior

Design paired methods deliberately, such as renew returning false and renew! raising, and never rely on a missing bang as proof of immutability.

for a principal

Set API naming guidance for the team so that bang and predicate methods mean the same thing across libraries that others will consume.

## Characters in a name, not operators A Ruby method name may end in `?`, `!` or `=`. The first two carry **no special behaviour**: the interpreter calls `overdue?` and `renew!` exactly like any other method. Their meaning is a **convention** that readers rely on, and Ruby's own documentation notes that it holds for the core library but may or may not hold for other libraries. ## The ? predicate convention A method ending in `?` answers a yes/no question. - It returns a **truthy or falsy** value. Many predicates return exactly `true` or `false`, such as `Array#empty?`, but that is not guaranteed. - `Integer#nonzero?` returns the number itself when it is not zero and `nil` when it is, which lets it be chained in sort comparisons. - `File.size?` returns the file size as an Integer, or `nil` when the file is missing or empty. - Ruby style names predicates `overdue?`, not `is_overdue` or `is_overdue?`. So code should test a predicate's result for truthiness (`if loan.overdue?`), never compare it with `== true`. ## What ! means: dangerous relative to its twin A bang method is the **more dangerous version of a method with the same name without the `!`**. "Dangerous" is relative to that twin: | Pair | Plain version | Bang version | |---|---|---| | `sort` / `sort!` | returns a new sorted array | sorts the receiver in place | | `upcase` / `upcase!` | returns a new string | changes the receiver | | `exit` / `exit!` | raises `SystemExit`, so `ensure` blocks and `at_exit` handlers run | ends the process immediately, no exit handlers | | `renew` / `renew!` (our loan class) | returns `false` when renewal is not allowed | raises when renewal is not allowed | In the core library the danger is almost always that the bang version **modifies its receiver**, and nearly every core bang method has a non-bang counterpart. In application code the same suffix commonly marks the version that **raises** instead of returning a failure value. ## What ! does not promise 1. **Not every mutating method has a bang.** `Array#push`, `Array#<<`, `Array#delete`, `Array#clear`, `String#concat`, `String#replace` and `Hash#delete` all change their receiver, with no `!`, because there is no safe twin to contrast them with. 2. **A bang does not always mean mutation.** `exit!` changes nothing about an object; it skips cleanup. 3. **A bang on its own is a smell.** If there is no `renew`, naming a method `renew!` gives readers nothing to compare against; use a descriptive verb instead. 4. **Return values differ by method.** Read each bang method's documentation rather than assuming it returns the receiver. ## Reading an unfamiliar API 1. **Look for the twin.** If `x!` exists, compare it with `x` to learn what the danger is: mutation, raising, or skipped cleanup. 2. **Do not treat a missing bang as a promise.** Check whether a method without `!` mutates; `Hash#update` changes the receiver and is simply another name for `Hash#merge!`. 3. **Read the return value in the documentation.** Bang and predicate methods each document what they return, and the conventions do not fix it. 4. **Test predicates for truthiness.** `if loan.overdue?` works whatever truthy value comes back. ## Designing a LibraryLoan - `overdue?` returns whether the due date has passed: a predicate with no side effects. - `renew` returns `false` when the loan has hit `MAX_RENEWALS`, and otherwise extends the due date and returns `true`. - `renew!` extends the due date too, but raises when the limit is reached, so callers that cannot handle failure use it. - `due_date=` is the setter for the date itself, a third suffix with a different meaning. In an interview, the strong answer states the two conventions, gives a counterexample to "`!` means mutation" in each direction, and explains that the language enforces none of it.

  • Why does Array#push mutate the array without ending in a bang?
    Because the bang convention marks the dangerous member of a pair, and `push` has no safe twin: appending to an array only makes sense in place. The same holds for `<<`, `delete`, `clear` and `concat`. Readers therefore cannot assume that a method without `!` leaves its receiver alone; they have to know the method.
  • Should a predicate method return exactly true or false?
    It is good practice for your own predicates, because callers may print or serialise the result. Ruby does not require it, and core methods break it on purpose: `nonzero?` returns the number or `nil`, and `File.size?` returns the size or `nil`. Callers should rely on truthiness, never on `== true`.

A bang is like a caution sign on one of two doors: it tells you that door is riskier than its neighbour, not what the risk is, and some risky doors have no sign at all.

saying these in an interview costs you the question

  • Ruby checks that a ? method returns true or false
  • The ! suffix means the method modifies its receiver, always
  • A method without ! never changes the object it is called on
  • Any method that mutates state should be given a bang
  • exit! is simply a louder alias of exit