In Ruby, what happens step by step when you write users.map(&:email), and what does the & do to the Symbol?
answer
- & turns an argument into the block
- non-Proc: call to_proc
- Symbol#to_proc sends the method
- first yielded value is the receiver
- a lambda since Ruby 3.0
basics
~20 sThe & marks :email as the block for map. Because a Symbol is not a Proc, Ruby converts it with Symbol#to_proc, a lambda that calls email on each yielded user, so the result equals users.map { |user| user.email }.
solid answer
~40 sA `&` before the last argument of a call means "use this object as the block". A `Proc` is used as it is; anything else is converted through `to_proc`. `Symbol#to_proc` returns a lambda that takes the first value it receives as the receiver and calls the method named by the symbol on it, passing any remaining values as arguments. So `users.map(&:email)` behaves like `users.map { |user| user.email }` and returns an array of addresses. Details interviewers probe: since Ruby 3.0 the proc is a lambda (`:email.to_proc.lambda?` is `true`, arity `-2`); the call is a public call, so a private `email` raises `NoMethodError`; and extra yielded values become arguments, so `users.each_with_index(&:email)` calls `email(index)` and raises `ArgumentError` for a zero-argument method. You cannot pass arguments of your own: `map(&:round(2))` is a `SyntaxError`.
code
ruby · 11 linesUser = Data.define(:name, :email)
users = [User.new(name: "Ada", email: "[email protected]"),
User.new(name: "Lin", email: "[email protected]")]
users.map(&:email) # => ["[email protected]", "[email protected]"]
to_email = :email.to_proc
to_email.lambda? # => true (Ruby 3.0+)
to_email.call(users.last) # => "[email protected]"
users.each_with_index(&:email) # ArgumentError: email receives the indexgo deeper
Recall that users.map(&:email) is shorthand for a block calling email on each element, and that & turns the symbol into a block through to_proc.
Explain the & conversion order (nil, Proc, to_proc), how Symbol#to_proc treats the first value as receiver and passes the rest as arguments, and the inject(&:+) case.
Cover the edge cases that bite in production: public-only calls raising NoMethodError on private readers, extra yielded values breaking zero-argument methods, and the Ruby 3.0 lambda change.
Set a readable convention for the shorthand in a shared code base: where &:name clarifies intent and where an explicit block or it communicates better to future maintainers.
## The & in a method call In Ruby, a **block** is normally written literally after a call. The **`&` prefix** on the last argument lets you supply the block as an object instead: ```ruby User = Data.define(:name, :email) users = [ User.new(name: "Ada", email: "[email protected]"), User.new(name: "Lin", email: "[email protected]") ] users.map(&:email) # => ["[email protected]", "[email protected]"] users.map { |user| user.email } # the same result ``` What Ruby does with `&obj` depends on the object: 1. **`nil`**: the call gets no block at all. 2. **A `Proc`**: that proc becomes the block unchanged. 3. **Anything else**: Ruby calls `obj.to_proc` and uses the returned `Proc`. If there is no `to_proc`, or it returns something that is not a `Proc`, the call raises `TypeError`. A `Symbol` falls into case 3, so the work is done by **`Symbol#to_proc`**. ## What Symbol#to_proc builds `:email.to_proc` returns a `Proc` that: - takes the **first** value it receives as the **receiver**; - calls the method named `email` on it; - passes any **remaining** values as arguments to that method. ```ruby to_email = :email.to_proc to_email.call(users.first) # => "[email protected]" to_email.lambda? # => true to_email.arity # => -2 ``` For `users.map(&:email)`, `map` yields each user, the proc calls `user.email`, and `map` collects the results. | Expression | Equivalent block | |---|---| | `users.map(&:email)` | `users.map { \|u\| u.email }` | | `names.each(&:strip!)` | `names.each { \|n\| n.strip! }` | | `[1, 2, 3].inject(&:+)` | `[1, 2, 3].inject { \|sum, n\| sum + n }` | The last row shows the "remaining values become arguments" rule: `inject` yields two values, so the proc calls `sum.+(n)`. ## Version note Since **Ruby 3.0**, `Symbol#to_proc` returns a **lambda**: `lambda?` is `true` and `arity` is `-2` (one required receiver plus any number of arguments). Earlier releases returned a regular proc. The change matters only when code inspects the proc, for example a library that branches on `lambda?` or `arity`. ## Edge cases interviewers ask about - **Visibility.** The method is called as a public call, so if `email` is `private`, `users.map(&:email)` raises `NoMethodError` even though `users.map { |u| u.send(:email) }` would work. - **Extra yielded values.** `each_with_index` yields the element and its index, so `users.each_with_index(&:email)` calls `user.email(0)` and raises `ArgumentError` for a method that takes no arguments. - **No extra arguments of your own.** `prices.map(&:round(2))` is a `SyntaxError`, because `&` takes one object and `:round(2)` is not an expression. Write a block: `prices.map { it.round(2) }`. - **Method missing.** If the receiver handles the name through `method_missing`, the proc still works, because it performs an ordinary method call. ## Three ways to write the same map | Form | Reads as | Notes | |---|---|---| | `users.map(&:email)` | call `email` on each | public call only, no extra arguments | | `users.map { it.email }` | same, Ruby 3.4+ | allows arguments and chains, e.g. `it.email.downcase` | | `users.map { \|user\| user.email }` | same, named | clearest once the block grows | All three return the same array for public, zero-argument readers. The differences only show up at the edges: visibility, arguments and what extra values the iterator yields. ## Common mistakes 1. **Chaining inside the shorthand.** `&:email` can name one method only; for `email.downcase` either chain two maps, `users.map(&:email).map(&:downcase)`, or write a block. 2. **Using it with iterators that yield several values.** `each_with_index`, `each_with_object` and `inject` pass extra values as arguments, which is useful for `inject(&:+)` and an `ArgumentError` for a zero-argument reader. 3. **Assuming a private reader works.** The shorthand is a public call, so a `private` `email` raises `NoMethodError`. ## When to use it `&:name` is idiomatic for **one method call with no arguments** on each element: `map(&:email)`, `select(&:active?)`, `sum(&:amount)`. As soon as you need arguments, a chain of calls or a condition, a literal block (or `it`, on Ruby 3.4+) is clearer than any trick with `&`. Readers of Ruby code expect the shorthand, so using it where it fits is a style point, not cleverness.
- Why does users.map(&:email) raise NoMethodError when email is a private method?The proc from `Symbol#to_proc` performs a public method call on the receiver, as if written `user.email` from outside the object. Private methods cannot be called with an explicit receiver that way, so Ruby raises `NoMethodError`. A literal block using `send` would bypass visibility, but exposing the reader publicly is usually the right fix.
- What does [1, 2, 3].inject(&:+) return, and why does it work?It returns 6. `inject` yields two values, the running total and the next element; the proc treats the first as the receiver and passes the second as the argument, so each step computes `total.+(element)`.
saying these in an interview costs you the question
- &:email calls a method named to_proc on each user
- Symbol#to_proc returns a regular proc, not a lambda, on Ruby 4.0
- &:email can call private methods because it uses send
- map(&:round(2)) passes 2 to round for each element
- Passing &:email to a method that yields two values ignores the second value