I discovered today that Ruby treats underscores a little bit differently when it comes to variable names.

Suppose, for the sake of argument, that we have a set of data that looks like this:

people = {
  "Alice" => ["green", "[email protected]"],
  "Bob"   => ["brown", "[email protected]"]
}

and we want to ignore eye colour and age and convert each element to a simple array containing name and email address. We might do this:

people.map { |name, fields| [name, fields.last] }

but it’s not very clear what fields.last is. Ruby has some fairly powerful destructuring assignment, so we can reach inside the structure with parentheses to make things more explicit:

people.map { |name, (eye_color, email)| [name, email] }

That’s better, but that unused variable is noise in this context. It doesn’t add to our understanding of what the code does.

In many languages (Haskell, Go, Clojure, and plenty more), it’s conventional to use an underscore to represent an unused variable. This is also recommended by Ruby style guides: 1, 2.

With that in mind, we can write:

people.map { |name, (_, email)| [name, email] }

and be fairly satisfied. But what if we’ve more elements to ignore?

people = {
  "Alice" => ["green", 34, "[email protected]"],
  "Bob"   => ["brown", 27, "[email protected]"]
}

No problem. Just re-use the underscore:

people.map { |name, (_, _, email)| [name, email] }

You can’t do it with any variable, though, at least in Ruby 1.9. It only works with variables that are called _:

people.map { |name, (x, x, email)| [name, email] }
# SyntaxError: (eval):2: duplicated argument name

The reason for this is found in Ruby’s parser, in shadowing_lvar_gen. All the normal checks for duplication are skipped iff the variable name consists of exactly one underscore.

Multiple underscores work in Ruby 1.8, too, but for a different reason: Ruby 1.8 doesn’t care about duplicated arguments in block parameters.

Meanwhile, neither 1.8 nor 1.9 care about duplication in multiple assignments:

a, a, b = 1, 2, 3
a, a, b = [1, 2, 3]

Both forms work without error (although I wouldn’t recommend relying on the value of a).

(Thanks to Ben for finding the pertinent line in parse.y.)