skip to content

Mutability & Frozen Literals

Ruby strings mutate in place: << appends, + allocates a copy, bang methods edit the receiver, and literals stay chilled until frozen. Interviewers probe FrozenError and aliasing bugs.

on this pageshow

explore

questions

5

In Ruby, why does calling name.strip! inside a method change the caller's string, while name = name.strip does not?

level: juniorimportance: must knowfreq 62%

answer

  1. references are passed by value
  2. bang edits the receiver
  3. reassignment rebinds a local only
  4. replace, << and clear have no bang

basics

~20 s

Ruby hands the method a reference to the caller's String object. strip! edits that object in place, so the caller sees it; name = name.strip builds a new string and rebinds only the method's local parameter.

solid answer

~40 s

Ruby passes **object references by value**: the parameter `name` and the caller's variable point at the same `String`. A bang method such as `strip!`, `upcase!` or `squeeze!` is the in-place variant of its non-bang twin, so it edits that shared object and the caller sees the change. `name = name.strip` calls the non-bang method, which returns a **new** string, and then rebinds the local `name`; the caller's variable still points at the untouched original. The `!` suffix is a convention meaning "the more dangerous version", not a language rule: `<<`, `replace`, `insert`, `prepend`, `clear` and `concat` all mutate without it. A method that does not own its argument should use the non-bang forms, which also keeps it safe when a caller passes a frozen string, where a bang method would raise `FrozenError`.

code

ruby · 12 lines
ruby
def shout!(msg) = msg.upcase!
def shout(msg)  = msg.upcase

greeting = +"hello"
shout(greeting)
greeting          # => "hello"
shout!(greeting)
greeting          # => "HELLO"

note = +"draft"
note.replace("final")   # no bang, still a mutator
note              # => "final"

go deeper

for a junior

Recall the pairs strip/strip! and upcase/upcase!: the bang version changes the string itself, the plain version returns a new one. Predict what the caller's variable shows afterwards.

for a middle

Explain pass-by-reference-value: mutation of the shared object is visible, reassignment of the parameter is not. Name mutators that have no bang, such as replace and <<.

for a senior

Point to the production bugs: helpers that clean caller-owned strings, constants altered for the whole process, and FrozenError when a frozen argument meets a bang method.

for a principal

Treat it as an API-contract question: when a library may mutate its arguments, how it signals that, and whether a team bans in-place edits on inputs.

## What a method actually receives When Ruby calls a method, each argument is a **reference to an object**, copied into the parameter. This is often summarised as *pass-by-reference-value*: - The method cannot make the caller's **variable** point somewhere else. Assigning to the parameter only changes the method's local. - The method **can** change the **object** the caller's variable points at, if the object is mutable and the method calls a mutator on it. Strings are mutable objects, so both effects are visible with them. ## Bang and non-bang pairs Many `String` methods come in pairs. The plain name returns a new string; the name ending in `!` edits the receiver in place. | Non-bang (returns a new string) | Bang (edits the receiver) | |---|---| | `strip` | `strip!` | | `upcase` | `upcase!` | | `capitalize` | `capitalize!` | | `squeeze` | `squeeze!` | | `chomp` | `chomp!` | So in this method: ```ruby def normalize!(name) name.strip! name.upcase! end customer = +" ada lovelace " normalize!(customer) customer # => "ADA LOVELACE" ``` the caller's `customer` changed, because both bang calls wrote into the one shared object. Rewritten without bangs: ```ruby def normalize(name) name = name.strip.upcase # new string, local rebinding end customer = +" ada lovelace " normalize(customer) # => "ADA LOVELACE" customer # => " ada lovelace " ``` the method returns the cleaned value and the caller's string is untouched. ## The ! suffix is a convention, not a rule The exclamation mark marks the **more surprising** of two related methods. For strings that nearly always means "mutates the receiver", but: 1. Several mutators have **no** `!` because they have no non-mutating twin: `<<`, `concat`, `replace`, `insert`, `prepend`, `clear` and `[]=` all change the receiver. 2. Some bang methods are dangerous for reasons other than mutation: `Kernel#exit!` exits without running `at_exit` handlers. 3. A method without `!` is not guaranteed to be pure; the suffix exists only where a pair exists. Reading the suffix as "the only way to mutate" is a classic source of aliasing bugs. ## Where it bites - **Helpers that clean their input.** A `normalize` helper that calls `strip!` on its argument rewrites the caller's form field, log line or cache key. - **Shared constants.** `DEFAULT_SIGNATURE = "Regards"` handed to a method that runs `upcase!` changes the constant for the rest of the process, or raises `FrozenError` if the file freezes literals. - **Collections.** `names.each { |n| n.strip! }` edits the strings inside the array. That is sometimes intended; `names.map(&:strip)` builds a new array of new strings instead. - **Frozen arguments.** If the caller passes a frozen string, bang methods such as `strip!` and `upcase!` raise `FrozenError` (`can't modify frozen String: "..."`), turning a style choice into a crash. ## Reading a method for side effects When reviewing code that receives strings, three quick checks catch most surprises: 1. **Does the method call a bang method, `<<`, `replace`, `insert`, `prepend`, `clear` or `[]=` on a parameter?** If so, the caller's object changes. 2. **Does it store the parameter** in an instance variable, array or hash and later mutate the stored value? The mutation still reaches the caller, just later. 3. **Does the method name end in `!`** when it mutates its argument or receiver and a non-mutating twin exists? If not, the side effect is invisible at the call site. A test that asserts the caller's string is unchanged after the call is the cheapest guard against a helper quietly switching from `strip` to `strip!` during a refactor. ## A working rule - Use the non-bang form on anything you did not create. - Use the bang form on a buffer the current method owns, when saving an allocation matters. - Name your own mutating methods with `!` when a non-mutating twin exists, so callers can see the side effect at the call site.

  • Name String methods that mutate the receiver even though they have no ! suffix.
    `<<`, `concat`, `replace`, `insert`, `prepend`, `clear` and element assignment with `[]=` all change the receiver. They have no `!` because there is no non-mutating twin for the suffix to distinguish them from.
  • What happens if the caller passes a frozen string to a method that calls strip! on it?
    The bang method tries to write into a frozen object and raises `FrozenError` with a message like `can't modify frozen String: "..."`. The non-bang `strip` works on frozen strings because it only reads the receiver.
  • Inside the method, can name = "other" make the caller's variable point at a different string?
    No. The parameter holds a copy of the reference, so reassigning it only changes the method's local variable. Only mutating the shared object is visible to the caller.

saying these in an interview costs you the question

  • Ruby passes strings by value, so a method can never change the caller's string
  • Every String method that mutates its receiver ends in an exclamation mark
  • Reassigning the parameter with name = name.strip updates the caller's variable
  • Bang methods work on a private copy of the receiver
  • The ! suffix always means in-place mutation and nothing else
open as a page

In Ruby, how do <<, + and += differ when building a string that another variable also references?

level: middleimportance: must knowfreq 66%

basics

~20 s

String#<< appends in place and returns the same object, so every variable holding it sees the change. + returns a new string, and s += x is shorthand for s = s + x, which rebinds only s.

open as a page

In Ruby, what does the # frozen_string_literal: true magic comment change, and which strings in that file stay mutable?

level: middleimportance: must knowfreq 56%

basics

~10 s

The comment makes every plain string literal in that one file frozen and deduplicated, so mutating one raises FrozenError. Interpolated literals, strings returned by methods, +"" buffers and strings from other files stay mutable.

open as a page

In Ruby 4.0, what is a chilled string literal, and why does mutating one usually run with no visible warning?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A chilled string is a literal from a file with no frozen_string_literal comment. It reports frozen? as false and mutates normally; the first mutation emits a deprecation warning, which is invisible unless deprecation warnings are enabled.

open as a page

In Ruby, what do unary +"..." and -"..." (String#+@ and String#-@) return, and when would you use each?

level: seniorimportance: should knowfreq 30%

basics

~20 s

+str returns str itself when it is already mutable without warning, otherwise an unfrozen copy. -str returns a frozen, deduplicated string equal to str, so equal strings share one object. Use + for buffers, - for repeated values.

open as a page