In Ruby 4.0, how do to_sym, Symbol#to_s, Symbol#name and %i[] literals convert between strings and symbols?
answer
- to_sym and intern are aliases
- id2name is an alias of to_s
- name: one cached frozen String
- to_s: new chilled String since 3.4
- %i no interpolation, %I interpolates
basics
~20 sString#to_sym (alias intern) finds or creates the symbol; Symbol#to_s (alias id2name) returns a new String that warns on mutation since Ruby 3.4; Symbol#name returns a cached frozen String; %i[a b] builds [:a, :b], and %I interpolates.
solid answer
~40 s`String#to_sym` and its alias `intern` return the symbol for that text, creating it only if it does not exist yet: `"in_transit".to_sym # => :in_transit`. Going back, `Symbol#to_s` and its alias `id2name` allocate a new String on each call; since Ruby 3.4 that String is chilled and emits a deprecation warning when mutated (visible with `-W:deprecated`), ahead of being frozen in a future release. `Symbol#name`, added in Ruby 3.0, returns the same frozen String every time, so it is the allocation-free choice for comparisons and logging. Literals cover the rest: `:"in-transit"` for names that are not identifiers, `:"status_#{code}"` with interpolation, `%s(pending)`, and `%i[pending in_transit delivered]` for an array of symbols, with `%I` when the words need interpolation.
code
ruby · 12 lines"in_transit".to_sym # => :in_transit
"in_transit".intern # => :in_transit
:in_transit.to_s # => "in_transit" (new String each call)
:in_transit.id2name # => "in_transit"
:in_transit.name.frozen? # => true
:in_transit.name.equal?(:in_transit.name) # => true
:"in-transit" # => :"in-transit"
%s[out for delivery] # => :"out for delivery"
%i[pending in_transit delivered] # => [:pending, :in_transit, :delivered]
code = 2
%I[status_#{code}] # => [:status_2]go deeper
Know to_sym and to_s for converting, the quoted :"..." form for names with hyphens or spaces, and %i[] for arrays of symbols.
Explain the difference between to_s and name, including allocation and frozenness, and why %i does not interpolate while %I does.
Audit code for mutation of to_s results before Ruby freezes them, and prefer name in hot paths that read symbol text.
Treat the 3.4 deprecation warnings as an upgrade signal: enable deprecation warnings in CI so mutation of to_s results surfaces before a release freezes them.
## Four conversions, two directions | Call | Returns | Allocates? | Notes | |---|---|---|---| | `"in_transit".to_sym` | `:in_transit` | only if the symbol is new | `String#intern` is the same method | | `:in_transit.to_s` | `"in_transit"` | a new String each call | alias `id2name`; chilled since Ruby 3.4 | | `:in_transit.name` | `"in_transit"` | no - the same frozen String | added in Ruby 3.0 | | `:in_transit.to_sym` | `:in_transit` | no | returns `self`; alias `intern` | `String#to_sym` looks the name up in the interpreter's symbol table and returns the existing symbol if there is one, so `"delivered".to_sym` and the literal `:delivered` are the same object. Only a name never seen before creates a new symbol. ## to_s versus name Both return the name without the leading colon, but they differ in what they give back: 1. **`Symbol#to_s`** creates a fresh `String` on every call. Historically that string was fully mutable. Since **Ruby 3.4** it is *chilled*: mutating it still works but emits a deprecation warning the first time, shown when deprecation warnings are enabled (`-W:deprecated` or `Warning[:deprecated] = true`), because a future release will return a frozen string. 2. **`Symbol#name`**, added in **Ruby 3.0**, returns the symbol's own cached, **frozen** string. `:delivered.name.frozen?` is `true`, and calling it twice returns the same object. 3. **`Symbol#id2name`** is simply an alias of `to_s`; **`Symbol#inspect`** is different again and returns the literal form with the colon, `":delivered"`. The practical rule: use `name` when you only read the text (comparisons, log fields, hash lookups by string), and `to_s` (or `name.dup`) only when you really need a mutable String. ## Literal forms - **Plain**: `:delivered`, valid for anything that reads as an identifier, an operator such as `:+`, or a setter name like `:status=`. - **Quoted**: `:"in-transit"` or `:"out for delivery"` for names that are not identifiers. Double quotes allow escapes and interpolation, `:"status_#{code}"`; single quotes, `:'status_#{code}'`, keep the text literally. - **`%s`**: `%s[pending]` is `:pending`, and `%s[out for delivery]` is `:"out for delivery"`. - **`%i` and `%I`**: whitespace-separated words become an array of symbols. `%i[pending in_transit delivered]` is `[:pending, :in_transit, :delivered]`. `%i` does not interpolate - `%i(#{1 + 1})` produces odd symbols such as `:"\#{1"` - while `%I(#{1 + 1})` gives `[:"2"]`. A backslash embeds a space: `%i[out\ for\ delivery]` is `[:"out for delivery"]`. ## A delivery-status example ```ruby STATUSES = %i[pending in_transit out_for_delivery delivered].freeze status = :out_for_delivery status.name # => "out_for_delivery" (frozen, cached) status.name.tr("_", " ") # => "out for delivery" (a new String) "delivered".to_sym.equal?(:delivered) # => true ``` `%i` keeps the list of statuses readable, `name` feeds text without allocating a copy of the symbol's name, and `to_sym` maps known text back to the same interned symbol. ## String-like methods on Symbol `Symbol` borrows a read-only slice of the String API, which makes quick checks possible without converting: | Method | Example | Returns | |---|---|---| | `length` / `size` | `:in_transit.size` | `10` | | `start_with?`, `end_with?` | `:out_for_delivery.start_with?("out")` | `true` | | `[]` / `slice` | `:delivered[0, 3]` | the String `"del"` | | `upcase`, `downcase`, `capitalize`, `swapcase` | `:delivered.upcase` | the Symbol `:DELIVERED` | | `succ` / `next` | `:a.succ` | `:b` | | `match?`, `=~` | `:in_transit.match?(/transit/)` | `true` | Note the split in return types: the case methods and `succ` return **symbols**, while `[]` returns a **String**. None of them changes the receiver, and there is no `<<`, `gsub!` or any other bang method. ## Traps - **`:in-transit` is not a symbol literal.** It parses as `:in - transit`, and Ruby raises `NameError` for the unknown `transit`. Quote it: `:"in-transit"`. - **Mutating `to_s` output** will break once Ruby freezes it; build a new string with `+` or `dup` instead of appending to the result. - **`%i` versus `%w`**: `%w[pending delivered]` gives Strings, `%i[pending delivered]` gives Symbols - an easy slip when a comparison later fails. - **`to_sym` on external text** creates symbols from whatever arrives; validate against a known list first.
- In Ruby 3.4 and later, what happens when code appends to the result of Symbol#to_s?The append still succeeds, because the returned String is chilled rather than frozen. The first mutation emits a deprecation warning when deprecation warnings are enabled, with `-W:deprecated` or `Warning[:deprecated] = true`, since a future Ruby will return a frozen String there. Fix such code by building a new string, for example `sym.name + "!"`, instead of mutating the result.
- In Ruby, why was Symbol#name added when Symbol#to_s already existed?`Symbol#to_s` has to return a new String each time, because callers were allowed to mutate it, so every call allocates. `Symbol#name`, added in Ruby 3.0, returns the symbol's own frozen String, the same object on every call, so read-only uses such as comparisons and logging avoid the allocation.
saying these in an interview costs you the question
- Symbol#to_s returns the same frozen String every time.
- Symbol#name returns a new mutable String on each call.
- %i[pending delivered] produces an array of Strings.
- :in-transit is a valid symbol literal.
- String#to_sym always creates a new Symbol even when the name already exists.