skip to content

Conventions & Comments

Ruby names carry meaning: snake_case methods, CamelCase classes, ? predicates, ! for the dangerous twin and = for setters. Interviewers check what ! does and does not promise.

on this pageshow

explore

questions

5

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
open as a page

In Ruby, which naming rules for a LibraryLoan class are enforced by the parser, and which are only snake_case and CamelCase conventions?

level: juniorimportance: must knowfreq 58%

basics

~20 s

The parser enforces only the first character: an uppercase initial makes a constant, so class and module names must be capitalised, while a lowercase or underscore initial makes a local or method. snake_case, CamelCase and SCREAMING_SNAKE_CASE are conventions.

open as a page

In Ruby, why must a LibraryLoan method write self.due_date = day instead of due_date = day to call its due_date= setter?

level: middleimportance: should knowfreq 48%

basics

~20 s

Inside a method, name = value without a receiver always assigns a local variable, so the due_date= setter never runs. An explicit receiver, self.due_date = day, makes Ruby call the method, and self works even when the setter is private.

open as a page

In Ruby 4.0, why might a file's # frozen_string_literal: true magic comment be ignored, and what syntax rules must magic comments follow?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Magic comments are read only from the comments at the top of a file, before any code. A frozen_string_literal line after a require, with trailing text, without its colon or with a non-boolean value is ignored.

open as a page

In Ruby, how do =begin and =end create a multi-line comment, and why do most Rubyists use consecutive # lines instead?

level: juniorimportance: nice to knowfreq 24%

basics

~20 s

Everything between =begin and =end is ignored, but both markers must start at the very beginning of their own lines, so they cannot be indented inside a class or method. Most Rubyists put # on each line instead, which works at any indentation.

open as a page