Destructuring assignment in Ruby

My post on underscores in Ruby attracted quite a lot of interest, particularly on the topic of destructuring assignment, so I thought I’d go into a bit more detail.

As mentioned previously, Ruby supports destructuring assignment. You can assign an array to multiple variables both in direct assignment:

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

and in block parameters:

[[1, 2, 3]].map { |a, b, c| "..." }

I’ll explain everything with the first form from here on, as it’s a little simpler. Unless I’ve missed something, all these forms should work equally well in both forms; let me know if you spot something that doesn’t. I’ll only be talking about Ruby 1.9, too, as Ruby 1.8 doesn’t support all of these syntactic constructs.

The outermost square brackets are optional when assigning variables, but I’ll use them throughout as I think it’s a little clearer in this context.

Multi-level destructuring

You can parenthesise any set of variables on the left hand side to mirror the structure on the right:

a, (b, c) = [1, [2, [3, 4]]]

a # => 1
b # => 2
c # => [3, 4]

but you don’t have to stop there. It’s turtles all the way down:

a, (b, (c, d)) = [1, [2, [3, 4]]]

a # => 1
b # => 2
c # => 3
d # => 4

‘Splatting’: assigning multiple elements

Extra values on the right hand side are usually discarded:

a, b, c = [1, 2, 3, 4, 5]

a # => 1
b # => 2
c # => 3

Prepending an asterisk—the ‘splat’ operator—to a variable tells it to gather up all the unassigned elements:

a, b, *c = [1, 2, 3, 4, 5]

a # => 1
b # => 2
c # => [3, 4, 5]

This works at the beginning of the list, too:

*a, b, c = [1, 2, 3, 4, 5]

a # => [1, 2, 3]
b # => 4
c # => 5

or in the middle:

a, *b, c = [1, 2, 3, 4, 5]

a # => 1
b # => [2, 3, 4]
c # => 5

But you can only use it once:

a, *b, *c = [1, 2, 3, 4, 5]

# stdin:81: syntax error, unexpected tSTAR
# a, *b, *c = [1, 2, 3, 4, 5]
#         ^

That is, you can only use it once at a given level. Each level of parentheses can have a splat:

*a, (b, *c) = [1, 2, [3, 4, 5]]

a # => [1, 2]
b # => 3
c # => [4, 5]

Ignoring elements

You can reuse an underscore to represent any element you don’t care about:

a, _, b, _, c = [1, 2, 3, 4, 5]

a # => 1
b # => 3
c # => 5

To ignore multiple elements, use a single asterisk—I’m going to call it a ‘naked splat’ for no better reason than that it sounds a bit amusing:

a, *, b = [1, 2, 3, 4, 5]

a # => 1
b # => 5

The same rules apply to naked splats as to splatted variables: you can only use one in any given level of parentheses, but you can reuse it at each level:

a, *, (*, b, c) = [1, 2, 3, [4, 5, 6, 7]]

a # => 1
b # => 6
c # => 7

I wouldn’t necessarily encourage you to leap straight out and start using destructuring assignment everywhere, but it has its place. Use it where it makes your code clearer, and remember: if you need to be compatible with Ruby 1.8, much of this won’t work.

Comments

  1. gnarmis

    Wrote at 2012-09-25 18:44 UTC using Safari 536.26.14 on Mac OS X:

    (Accidentally posted on an unrelated post. Sorry about that)

    Nice post! This means you can do this:

    def with coll, &fun; coll.map &fun; end

    with [[1,2]] {|x,y| puts x+y} #=> 3

    Looks kinda nice, almost like having `let`.
  2. Ohm

    Wrote at 2012-09-25 19:53 UTC using Chrome 22.0.1229.64 on Mac OS X:

    I love the ‘naked splat’ it means you do not have to write:

    arr = [1,2,3,4,5]
    a, b = arr.first, arr.last

    but can instead just do either

    arr = [1,2,3,4,5]
    a, *, b = arr

    or as you have written

    a, *, b = [1,2,3,4,5]