In Ruby, what do unary +"..." and -"..." (String#+@ and String#-@) return, and when would you use each?
answer
- plus: mutable, minus: frozen
- +@ dups only when needed
- -@ returns a deduplicated string
- dedup is the alias of -@
- method call binds tighter than unary
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.
solid answer
~50 s`String#+@` answers "give me something I may mutate": it returns the receiver if it is not frozen and would not warn, otherwise `dup`. That makes `buf = +""` the standard mutable buffer under `# frozen_string_literal: true`, and it replaced the `str.dup if str.frozen?` idiom. `String#-@`, aliased as `dedup`, answers "give me the one shared frozen copy": it returns a frozen string from Ruby's deduplication table, so `(-a).equal?(-b)` is true for two plain `String` objects with the same content and encoding. It does not freeze a mutable receiver; it returns a frozen copy. Use it for values repeated many times in memory, such as parsed status codes or carrier names kept in thousands of records, and for constants. Mind precedence: `-"x".upcase` applies `-` to the result of `upcase`, which is why `dedup` reads better in chains.
code
ruby · 10 lines# frozen_string_literal: true
buf = +"" # mutable, source encoding
buf << "ALERT"
lines = ["sent\n", "sent\n", "failed\n"]
statuses = lines.map { |l| l.chomp.dedup }
statuses.map(&:object_id).uniq.size # => 2, one per distinct status
(-"x").frozen? # => true
-"sms".upcase # same as -("sms".upcase)go deeper
Recall that +"" gives a mutable string and -"" gives a frozen one; use +"" when you need a buffer in a frozen-literal file.
Explain when +@ returns the receiver and when it copies, and what dedup returns for two equal strings built at run time.
Show where dedup cuts memory, such as repeated statuses parsed from large reports, and the precedence trap that makes the dedup alias safer in chains.
Judge when deduplication pays: it saves memory only when values really repeat, and on mostly unique input it adds a table lookup per string for no gain.
## Two unary methods on String Ruby lets classes define unary operators. For `String`, the prefix forms `+str` and `-str` call the methods `String#+@` and `String#-@`. They are the two halves of Ruby's frozen-string toolkit: | Expression | Returns | Frozen? | Same object as receiver? | |---|---|---|---| | `+str` on a mutable string | `str` itself | no | yes | | `+str` on a frozen or chilled string | `str.dup` | no | no | | `-str` on a mutable string | the deduplicated frozen string equal to `str` | yes | no | | `-str` on a frozen string | the deduplicated frozen string equal to `str` | yes | possibly, if `str` itself is the registered copy | | `str.dedup` | same as `-str` | yes | as above | ## String#+@: a string you may mutate `+str` is a cheap way to guarantee mutability: - If the receiver is **not frozen** and mutating it would not emit a warning, it returns the receiver. No allocation. - If the receiver is **frozen**, or **chilled** (a Ruby 3.4+ literal from a file without a `frozen_string_literal` comment), it returns a `dup`, an unfrozen copy. This makes it the idiomatic buffer starter: ```ruby # frozen_string_literal: true def broadcast_body(lines) body = +"" # "" alone would be frozen here lines.each { |l| body << l << "\n" } body end ``` It also replaced the older idiom `str = str.dup if str.frozen?`, and since Ruby 3.4 it handles chilled literals the same way, so `+"text"` never triggers the chilled-string warning. Compared with `String.new`: `String.new` with no argument returns an empty string in the binary `ASCII-8BIT` encoding, while `+""` keeps the source encoding (normally UTF-8). For text buffers `+""` is the safer default. ## String#-@: one frozen copy per distinct value `-str` returns a **frozen** string equal to `str`, taken from Ruby's internal table of deduplicated strings: 1. If an equal string is already in the table, you get that object. 2. Otherwise the value is registered: a frozen receiver can itself become the shared copy, while a mutable receiver gets a frozen copy registered in its place. 3. A mutable receiver is **not** frozen in place; it stays mutable and you receive a separate frozen string. The result: every call with equal content returns the same object. ```ruby a = "carrier-" + "vodafone" b = "carrier-" + "vodafone" a.equal?(b) # => false (-a).equal?(-b) # => true (-a).frozen? # => true ``` Where it pays off: - **Repeated values from parsing.** A delivery report with a million rows but a dozen distinct status strings can keep a dozen objects instead of a million. - **Constants.** `CONFIG_PATH = -"config/sms.yml"` yields a frozen, shared constant even without the magic comment. String literals in a file with `# frozen_string_literal: true` are already frozen and deduplicated, so `-` on them adds nothing. ## Precedence and the dedup alias Unary operators on a literal bind **more loosely than a method call**: - `-"sms".upcase` is `-("sms".upcase)`: the deduplicated form of `"SMS"`. - `(-"sms").upcase` returns a new unfrozen `"SMS"`, because `upcase` builds a new string. When chaining, the alias `dedup` avoids the parentheses: `status.strip.dedup`. ## Common mistakes - Treating `+str` as always allocating. It copies only frozen or chilled receivers, so it is safe to use defensively on hot paths. - Expecting `-str` to make the original immutable. The receiver keeps its state; only the returned object is frozen. - Deduplicating strings that are almost all unique, such as message bodies or phone numbers. Each call still hashes the string and looks it up, and nothing is saved. - Mutating a deduplicated string after reaching it through another path. It is frozen, so the attempt raises `FrozenError`, and it may be an object other code relies on. ## Choosing - Need to **append or edit**? Start from `+str`. - Need to **keep many copies** of the same text, or a constant? Store `-str` (or `str.dedup`). - Never mutate the result of `-str`: it is frozen, and it may be the same object other code is holding.
- Does -str freeze the receiver when str is mutable?No. `String#-@` leaves a mutable receiver mutable and returns a separate frozen string equal to it, taken from or added to the deduplication table. Only an already frozen receiver can itself become the shared copy.
- Why prefer +"" over String.new for a text buffer?`String.new` with no argument returns an empty string encoded as `ASCII-8BIT`, a binary encoding. `+""` returns a string in the source encoding, normally UTF-8, which is what text built from literals expects.
- What does +str cost when str is already a mutable, unchilled string?Nothing: `String#+@` returns the receiver itself, with no copy. It only allocates when the receiver is frozen or chilled.
saying these in an interview costs you the question
- +str always allocates a copy, even when str is already mutable
- -str freezes the receiver in place
- Two equal strings passed through -@ are still different objects
- String.new and +"" both return a UTF-8 buffer
- -"sms".upcase applies the minus before calling upcase