skip to content

In Ruby, what separates explicit to_s, to_a and to_h from implicit to_str, to_ary and to_hash, and when should a class define the implicit ones?

level: seniorimportance: should knowfreq 42%

answer

  1. a request versus a claim
  2. core methods call only the implicit ones
  3. a, b = obj calls to_ary
  4. wrong return type is its own TypeError
  5. only if it can stand in

basics

~20 s

Explicit converters such as to_s, to_a and to_h are representations you ask for, and most classes may define them. Implicit ones such as to_str, to_ary and to_hash claim the object is that type, so core methods call them automatically.

solid answer

~50 s

Explicit methods produce a representation on request: `42.to_s`, `(1..3).to_a`, `nil.to_h`. Core methods do not call them to accept an argument. Implicit methods promise the object can stand in for a core type, so core methods call them for you: `String#+` calls `to_str`, multiple assignment and block-parameter destructuring call `to_ary`, `**obj` and `Hash#merge` call `to_hash`, and `Array#[]` calls `to_int` on an index. Without them you get `TypeError` such as `no implicit conversion of Quantity into String`; if they return the wrong class, `TypeError` names the method. Define them only on a class that genuinely is that type in all but name, such as a string or array wrapper. Adding `to_str` to a `Quantity` class would make `"Qty: " + quantity` succeed and hide type mistakes; Ruby's own docs say to define `to_ary` only if the object can be used in place of an Array.

code

ruby · 14 lines
ruby
class Quantity
  def initialize(count)
    @count = count
  end

  def to_s = "#{@count} pcs"
  def to_i = @count
end

qty = Quantity.new(6)
"Order: #{qty}"      # => "Order: 6 pcs"
Integer(qty)         # => 6, Integer() falls back to to_i
"Order: " + qty      # TypeError: no implicit conversion of Quantity into String
[10, 20, 30][qty]    # TypeError: no implicit conversion of Quantity into Integer

go deeper

for a junior

Recall the pairs: to_s and to_str, to_a and to_ary, to_h and to_hash, and that core methods call only the second of each pair.

for a middle

Explain where core Ruby calls each implicit method, multiple assignment, keyword splats, String#+ and indexing, and the two TypeError messages.

for a senior

Decide whether a domain class should define an implicit converter, weighing convenience against silent acceptance, and spot a misplaced to_str or to_ary in review.

for a principal

Set library conventions for value objects: explicit converters by default, implicit ones only for true wrappers, documented as part of the public contract.

## Two kinds of conversion method Ruby pairs each core type with two conversion methods: | Target | Explicit (a request) | Implicit (a claim) | |---|---|---| | String | `to_s` | `to_str` | | Array | `to_a` | `to_ary` | | Hash | `to_h` | `to_hash` | | Integer | `to_i` | `to_int` | - An **explicit** conversion answers "give me a representation of yourself as this type". Many unrelated classes define them: `Integer#to_s`, `Range#to_a`, `Struct#to_h`, `NilClass#to_a`. You call them when you have decided a conversion is appropriate. - An **implicit** conversion says "I *am* this type, in all but class". Few core classes define them: `String#to_str`, `Array#to_ary`, `Hash#to_hash`, and the numeric classes' `to_int`. Core methods call them automatically when they need an argument of that type. ## Where core Ruby calls the implicit methods The implicit methods are hooks the interpreter and core library use to accept objects that are not literally a `String`, `Array` or `Hash`: - **`to_str`**: `String#+`, `String#concat`, and path arguments to `File` methods, which try `to_path` first. - **`to_ary`**: multiple assignment `a, b = obj`, block parameters `each { |x, y| ... }` receiving a single object, `Array#+`, `Array#flatten`. - **`to_hash`**: the `**obj` keyword splat and `Hash#merge`. - **`to_int`**: array indexing `list[obj]` and `Integer()` before it falls back to `to_i`. None of these call `to_s`, `to_a` or `to_h`. That is why `[1, 2, 3][1.9]` returns `2` (`Float#to_int` truncates), but an object with only `to_i` raises `TypeError` with `no implicit conversion of Quantity into Integer` when used as an index. ## What the errors say The implicit protocol produces two distinct `TypeError` messages: 1. **Missing method**: `no implicit conversion of Quantity into String`. The object has no `to_str`. 2. **Wrong return type**: `can't convert Sku to String (Sku#to_str gives Integer)`. The method exists but broke its promise. Explicit conversion functions like `Integer()` report failure as `can't convert X into Integer` instead, because they are not the implicit protocol. ## Core classes that deliberately lack implicit converters The standard library applies the rule strictly, which is instructive: - **`Symbol`** has `to_s` but no `to_str`, so `"total" + :qty` raises `TypeError` even though a symbol is a name. - **`Pathname`**, a core class in Ruby 4.0, defines `to_path` (an alias of `to_s`) so `File` methods accept it, but not `to_str`, so string concatenation with a path raises. - **`Integer`** has `to_s` but no `to_str`, which is why `"Qty: " + 6` fails. Each of these classes has a perfectly good textual form; none of them *is* a string. ## When to define an implicit method Ruby's method documentation says it directly: define `to_ary` only if you can use your object in place of an Array. The same reasoning applies to the others. A checklist: 1. **Is it a wrapper around exactly that type?** A `Sku` wrapping a code string, which should work anywhere a string does, is a candidate for `to_str`. 2. **Would silent acceptance hide mistakes?** A `Quantity` is a number with a unit. Giving it `to_str` would let `"Qty: " + quantity` succeed, and also any accidental string operation. 3. **Does destructuring make sense?** A two-field `LineItem` with `to_ary` lets `name, qty = item` work, but also causes block parameters to auto-splat it, which can surprise callers. 4. **Prefer explicit methods otherwise.** Define `to_s` for display, `to_h` for serialisation, `to_a` for enumeration, and let callers convert on purpose. ## Worked example: grocery order objects In a grocery-order importer, `Quantity` wraps a count parsed from a CSV cell. It defines `to_s` returning `"6 pcs"` and `to_i` returning `6`, but neither `to_str` nor `to_int`. - `"Order: #{qty}"` works, through the explicit `to_s`. - `Integer(qty)` works, because `Integer()` falls back to `to_i`. - `"Order: " + qty` raises `TypeError`, exactly as intended. - `list[qty]` raises `TypeError`, because a quantity is not an index. A `LineItem` that holds a name and a quantity might define `to_ary` so `name, qty = item` reads naturally, accepting that any block receiving one `LineItem` with two parameters will destructure it too.

  • What happens if a class's to_str returns an Integer?
    Core methods that call `to_str` check the result. `"x" + obj` raises `TypeError` with `can't convert Sku to String (Sku#to_str gives Integer)`, a different message from the missing-method case, so the bug points at the broken conversion rather than at the caller.
  • Why does Integer(obj) work when obj defines only to_i, while list[obj] raises?
    `Kernel#Integer` is an explicit conversion function that tries `to_int` and then falls back to `to_i`. Array indexing uses only the implicit `to_int`, because it accepts objects that are integers in all but class, so an object with just `to_i` raises `no implicit conversion of Quantity into Integer`.

saying these in an interview costs you the question

  • to_s and to_str are aliases on every class
  • Defining to_str is good practice for any class with a text form
  • String#+ calls to_s on its argument
  • to_ary is just a faster version of to_a
  • A to_str that returns an Integer is silently accepted