Destructuring with Ruby

One of my side projects has been coding a bot for the Google AI Challenge. I have chosen to implement my bot in Clojure as a good excuse to learn the language with a problem that has some meat to it. One of the great things about learning a new language is learning some techniques that can help you with your day job. The one I'd like to talk about today is destructuring.

The Basics

So what is destructuring? The most concise definition I found is from Common Lisp the Language. Destructuring allows you to bind a set of variables to a corresponding set of values anywhere that you can normally bind a value to a single variable. It is a powerful feature of Clojure that lets you write some very elegant code. For more information about Clojure's features, I recommend you check out Jay Field's blog post on the subject. While destructuring in Ruby is not quite as powerful as Clojure, you can still do some cool stuff. Let's kick this off with an example.

x, y = [1, 2]
x # => 1

If you have done Ruby for a while, you have probably seen some code like that before. One place where it can be quite useful is to return multiple values from a function like so: (apologies for the contrived example)

def min_max(array)
  [array.min, array.max]
end
min, max = min_max([3,5,2])
min # => 2
max # => 5

The Splat Operator

Ruby has some more tricks up its sleeve. One of these is the splat (*) operator. Splat will perform two different operations depending on which side of the assignment it is used. The operation you've most likely run into is called slurp or collect. This takes a variable number of arguments and collects it into an array. Its often used to define methods that can take a variable number of arguments. One caveat is that you can only splat the last parameter to a method. Let's refactor our last example to use splat.

def min_max(*values)
  [values.min, values.max]
end
min, max = min_max(3,5,2)
min # => 2
max # => 5

Splat's other mode of operation is called split. This will convert an array into a list of arguments. This is often used when you have an array and you need to call a method with the arguments of the array. Here is an example.

def divide(numerator, denominator)
  numerator / denominator
end
divide(*[4,2]) # => 2

Let's go back to our first example. What happens if our array has more elements than the number of variables we supplied?

x, y = [1, 2, 3]
x # => 1
y # => 2

Oh snap, we lost 3! Not to worry, we can use the splat operator to slurp up the rest of the array elements.

first, *rest = [1, 2, 3]
first # => 1
rest # => [2, 3]

Hey, now we are on to something. If you have looked into Clojure, Scheme, or another Lisp variant that pattern should look familiar. There is a bevy of algorithms that take very complicated problems and break them down into much simpler problems using recursion and a destructuring similar to the one that we have above.

Destructuring Block Arguments

One of my favorite uses of destructuring in Ruby is inside of a block argument. The syntax for this is that anything within parenthesis will be destructured using the rules we've outlined above. Again, here are some examples.

triples = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

triples.each { |(first, second, third)| puts second }
# 2
# 5
# 8

triples.map { |(first, *rest)| rest.join(' ') } # => ["2 3", "5 6", "8 9"]

Destructuring Hashes

Thus far, we have only talked about destructuring arrays. We can also destructure hashes in a very interesting way. A Ruby Hash has a method called values_at that takes a list of keys and returns the values for those keys in th order they were given. Check it out.

hash = {:a => 1, :b => 2, :c => 3}
a, b = hash.values_at(:a, :b)
a # => 1
b # => 2

We can also do block-based destructuring with hashes. One very common use case is using inject. Let's say we wanted to write our own implementation of Ruby's Hash#invert method. We could do that using inject with some destructuring.

hash = {:a => 1, :b => 2}
hash.inject({}) { |new_hash, (key, value)| new_hash.merge(value => key) }
# => {1 => :a, 2 => :b}

In the words of Billy Mays, who may rest in peace, "but wait, there's more". We can destructure our destructuring.

hash = {:a => [1, 2, 3], :b => [4, 5, 6]}
hash.inject({}) { |new_hash, (key, (first, second, third))| new_hash.merge(second => key) }
# => {2 => :a, 5 => :b}

I know, that's nuts. I'm not sure if that would ever be useful, but it's a nice trick to have up your sleeve. Let take it one step further, will splat work in there?

hash = {:a => [1, 2, 3], :b => [4, 5, 6]}
hash.inject({}) { |new_hash, (key, (first, *rest))| new_hash.merge(rest => key) }
=> {[2, 3]=>:a, [5, 6] => :b}

Hells yes it does.

Destructuring Objects

As far as I know, Ruby does not have any useful native facility to perform destructuring on an arbitrary object. One of the beautiful things about the language is we can add features we think are missing. Let's give it a shot. We will use the Hash#values_at method as inspiration.

class Object
  def values_at(*attributes)
    attributes.map { |attribute| send(attribute) }
  end
end

class Dog
  attr_reader :color, :breed, :weight

  def initialize(color, breed, weight)
    @color, @breed, @weight = color, breed, weight
  end
end

dog = Dog.new("black", "lab", "heavy")
color, breed = dog.values_at(:color, :breed)
color # => "black"
breed # => "lab"

Ignore the naivety of the implementation, I know there are better ways to monkey patch. That aside, its a fairly cool concept. This would also allow you to use Objects and Hashes interchangeably when destructuring.

In Conclusion

If you've made it this far with me, I'm impressed. I hope that I've shown you some pretty cool things you can do with destructuring to help you write more elegant Ruby. Did I miss something? If so, call it out in the comments.

blog comments powered by Disqus