In Progress
Unit 1, Lesson 21
In Progress

Splat Group

Video transcript & code

Lately we've been looking at Ruby's splat operator. We've seen how it can be used to "destructure" arrays into named variables, as well as to "slurp" a series of values up into a single variable. Today, I want to talk about a related language feature: the ability to destructure recursively using groups. As with previous examples, we'll demonstrate the technique in the context of regular assignment, and then apply what we learn to block argument assignment.

Consider an array which represents the abstract syntax tree of a simple nested arithmetic expression. In classic Lisp style, each array consists of an operator, followed by the arguments to that operator. This expression represents the multiplication of 3 by a sub-expression. The sub-expression represents the addition of 2 and 5.

expr = [:*, 3, [:+, 2, 5]]

Lets say we want to extract the components of this expression into local variables, using destructuring assignment. We could start by breaking out the outer expression into an operator and two factors.

expr = [:*, 3, [:+, 2, 5]]
op, f1, f2 = expr

op                              # => :*
f1                              # => 3
f2                              # => [:+, 2, 5]

Then we could break out the nested expression into an operator and two terms:

expr = [:*, 3, [:+, 2, 5]]
op, f1, f2 = expr
inner_op, t1, t2 = f2

inner_op                        # => :+
t1                              # => 2
t2                              # => 5

But what if all we really care about is the inner expression? It would be nice if we could extract it directly without an intermediate variable. And, as a matter of fact, we can. By "grouping" arguments on the left side of the assignment, we can tell Ruby to assign elements of nested arrays directly to variables names.

expr = [:*, 3, [:+, 2, 5]]
op, f1, (inner_op, t1, t2) = expr

op                              # => :*
f1                              # => 3
inner_op                        # => :+
t1                              # => 2
t2                              # => 5

One way to think about this is that we are "mirroring" the structure of the nested array using parentheses. This mirrored structure gives Ruby a template with which to match up values from the structure on the right to names on the left.

Continuing in the Lisp theme, what if operators like + can take an arbitrary number of terms? We can use the splat operator within groups to collect any number of elements of a sub-expression into a new array. So, for instance, here's a version of the code where the inner addition expression has three terms. Instead of providing a variable for each term, we slurp them all up into a variable named ts.

expr = [:*, 3, [:+, 2, 5, 7]]
op, f1, (inner_op, *ts) = expr

op                              # => :*
f1                              # => 3
inner_op                        # => :+
ts                              # => [2, 5, 7]

Now let's apply this to something closer to a real-world situation. Supposing we want to step through entries in a Hash of sandwiches made by my favorite cafe. Furthermore, we want to assign a number to each one for ease of ordering.

So we send #each_with_index to the list of sandwiches, along with a block. Let's take a look at the arguments yielded to the block by #each_with_index by splatting all of the arguments into a single array and then inspecting that array.

menu = { 
  'Jacked Up Turkey'     => '$7.25',
  'New York Mikey'       => '$7.25',
  'Apple Grilled Cheese' => '$5.25' 
}

menu.each_with_index do |*args|
  puts args.inspect
end
# >> [["Jacked Up Turkey", "$7.25"], 0]
# >> [["New York Mikey", "$7.25"], 1]
# >> [["Apple Grilled Cheese", "$5.25"], 2]

At each iteration, #each_with_index passes two arguments into the block. The first is a two-element array consisting of the current hash key/value pair. the second is an integer representing the current index.

If we want to print all of these pieces of info, one way to do it is using an intermediate variable as we did in the assignment example previously. The pair argument is our intermediate variable, which we then split out into name and price.

menu = { 
  'Jacked Up Turkey'     => '$7.25',
  'New York Mikey'       => '$7.25',
  'Apple Grilled Cheese' => '$5.25' 
}

menu.each_with_index do |pair, i|
  name, price = pair
  puts "#{i+1}: #{name} (#{price})"
end
# >> 1: Jacked Up Turkey ($7.25)
# >> 2: New York Mikey ($7.25)
# >> 3: Apple Grilled Cheese ($5.25)

However, we can do one better than this. As you probably recall, any syntax we can use on the left side of an assignment, we can also use in an argument list. Instead of an intermediate pair argument, let's put a group in the block argument list. The group consists of name and price variables.

menu = { 
  'Jacked Up Turkey'     => '$7.25',
  'New York Mikey'       => '$7.25',
  'Apple Grilled Cheese' => '$5.25' 
}

menu.each_with_index do |(name, price), i|
  puts "#{i+1}: #{name} (#{price})"
end
# >> 1: Jacked Up Turkey ($7.25)
# >> 2: New York Mikey ($7.25)
# >> 3: Apple Grilled Cheese ($5.25)

Ruby sees the group, and matches the first and second elements of the first block argument to the name and price variables, respectively. As a result we no longer have any need of an intermediate pair variable which exists only to be broken up.

OK, that's it for today. Happy hacking!

Responses