In Progress
Unit 1, Lesson 21
In Progress

Detect Map

Video transcript & code

Let's say we run a trendy brewpub. We pride ourselves in our extensive stock of unique and obscure beer styles from a variety of boutique breweries around the world.

When a customer asks for a particular style of beer, we look through our extensive beer cellar and give them a bottle of beer in that style.

Of course, some styles are too obscure even for us, and in that case we come up nil.

require "./beer_cellar"

lookup_beer("Antarctic Pale Ale")
# => #<struct Beer
#     style="Antarctic Pale Ale",
#     name="Icebreaker Brewery Pingu Pale">

lookup_beer("Lager")
# => nil

It takes a long time for someone to go searching through the aisles of our beer vaults and then come back with a bottle.

Given this fact, and since we can't always guarantee we'll find a customer's first choice of beer style, we prefer that they give us a list of styles.

We can then take the list and search through stacks one style at a time, until we find a match.

We're using detect here, which will return the first item for which the block returns a truthy value.

For instance, a customer might hand us a list with "Skinny Stout" and "Blue IPA" listed. We take the list, and find that while we're all out of skinny stouts, we do indeed have a blue IPA in stock.

require "./beer_cellar"

def find_beer(*style_list)
  style_list.detect{|style| lookup_beer(style)}
end

find_beer("Skinny Stout", "Blue IPA")
# => "Blue IPA"

Except… whoopsie. We just returned the style of beer that we found. It would have been better if we had come back with an actual beer.

Let's fix this. We assign the found style to a local variable.

Then, so long as a style was found, we look it up again to get the actual beer instance.

Now when we search for a list of styles, we get the actual beer back instead of just the name of a style.

require "./beer_cellar"

def find_beer(*style_list)
  in_stock_style = style_list.detect{|style| lookup_beer(style)}
  in_stock_style && lookup_beer(in_stock_style)
end

find_beer("Skinny Stout", "Blue IPA")
# => #<struct Beer style="Blue IPA", name="Indy's Inscrutapale">

This works, but there's something seriously smelly about this code. We're calling lookup_beer twice: once to determine if a beer can be found for the give style, and then a second time to retrieve the beer and return it.

Let's see if we can rewrite this so it only calls lookup_beer once.

We'll send #map to the list of beer styles.

Inside the block, we map from the style to the found beer for that style—if any.

The resulting list may contain nil values for any styles that couldn't be found.

So we compact them out.

Then we take the first value from the compacted list.

Let's try this out. We'll pass in a list of 3 desired styles. We get a hit on the very first one.

However, even though we've eliminated the second lookup_beer call from our code, this method call actually had to do more beer lookups than the last one we tried. How can we tell? Well, it just so happens that we've instrumented the lookup_beer method with a call counter.

When we check the counter, we see that it has been incremented three times.

Why is this? Remember, map always executes its block for ever member of a collection.

So even though we only returned the first found beer, we actually looked up all three styles in our cellar.

require "./beer_cellar"

def find_beer(*style_list)
  style_list
    .map{|style| lookup_beer(style)}
    .compact
    .first
end

find_beer("Blue IPA", "Stinky Saison", "Norwegian Blue")
# => #<struct Beer style="Blue IPA", name="Indy's Inscrutapale">

$lookups                        # => 3

Our bar helpers are getting tired of running up and down the cellar steps. They demand we optimize this process.

One way we could fix this problem is by rewriting the code in a more imperative style.

We start by initializing a found variable to nil.

Then we iterate over the beer style list with #each.

For each style, we do our lookup and assign the result to the found variable.

Then we break if the found variable is non-nil.

Finally we return the value of the found variable.

Let's try this version out. Given the same style list, we get the same beer returned.

But this time, the lookup counter reads 1 instead of 3.

require "./beer_cellar"

def find_beer(*style_list)
  found = nil
  style_list.each do |style|
    found = lookup_beer(style)
    break if found
  end
  found
end
find_beer("Blue IPA", "Stinky Saison", "Norwegian Blue")
# => #<struct Beer style="Blue IPA", name="Indy's Inscrutapale">

$lookups                        # => 1

So great, we've solved our efficiency problem. But this is clunky imperative code. We're a hip, happening modern brewpub! Can't we do this in a more slick, functional way??

We can, but the technique is a little non-obvious.

We'll go back to sending #detect to the style_list.

This time, we assign the found beer—if any—to a found variable.

Now, if we actually found a matching beer, we want to exit from the detect iteration early, so we don't do any needless lookups.

In order to do this, we follow this statement with and break. Here we're using the and control-flow operator we learned about in episode #125.

But if we left this code as-is, we'd be back to returning the found style not the found beer. Because #detect returns the matching item from its receiver, not the result of the block.

Except… back in episode #71, we learned that when given a value, break overrides the return value of the block it is exiting from.

So we add the found variable as the argument to break.

Now, when this sub-expression is invoked, the return value of the whole #detect call will be forced to be the found beer.

Let's call this method one more time. We give it a list of beer styles as usual.

This time, the code has to make 2 lookups before finding a "Belgian Whitebock" beer.

require "./beer_cellar"

def find_beer(*style_list)
  style_list.detect do |style|
    found = lookup_beer(style) and break found
  end
end
find_beer("Decaf Porter", "Belgian Whitebock", "Hot & Sour")
# => #<struct Beer
#     style="Belgian Whitebock",
#     name="Babelfish Brewing Ole' Zebra">

$lookups                        # => 2

Now, this might all seem like an elaborate and contrived example to set up a fiddly little functional programming puzzle. But this is actually a scenario that I find crops up fairly frequently: I want to find the first item in a list for which a block returns non-nil, but rather than returning the item itself, I want to return the value of the block for the matching item.

This is the most elegant solution I've found for this particular scenario, and so I thought I'd share it with you. Happy hacking!

Responses