In Progress
Unit 1, Lesson 21
In Progress

Break With Value

Video transcript & code

In the last episode we explored how the break keyword is for more than just breaking out of loops. It can force an early return from any method that yields to a block. As an example, we forced this method that yields a series of names to return early when a name beginning with 'S' was found.

def names
  yield "Ylva"
  yield "Brighid"
  yield "Shifra"
  yield "Yesamin"
end
names do |name|
  puts name
  break if name =~ /^S/
end
Ylva
Brighid
Shifra

Let's examine the return value of that method:

result = names do |name|
  puts name
  break if name =~ /^S/
end
result
nil

The method call returns nil. Now let's say that instead of printing out the yielded names within the block, we wanted to capture the first name that begins with 'S' in a variable, before terminating the method. One way we could do that is to declare a local variable before the method is invoked. Then when the matching name is found, assign it to the result variable before the break.

result = nil
names do |name|
  puts name
  if name =~ /^S/
    result = name
    break
  end
end
result
Shifra

This works. But it's not very pretty.

We can accomplish this a lot more elegantly by making use of another feature of the break keyword: the fact that it can take an optional argument. Let's rewrite the method to give break the matching name as an argument.

result = names do |name|
  puts name
  break name if name =~ /^S/
end
result
Shifra

This produces the result we want, and the code is short and sweet. But what exactly happened here?

Here's how it works. When break is passed an argument, it effectively overrides the return value of the method it is breaking out of. So by passing the matching name to break, we forced the names method to return that string instead of a nil.

Now that we've seen what's possible with break, let's apply it to a slightly less contrived example. Let's say we are searching files for a line matching a particular pattern. One way we can do that is to grab the lines enumerator that an open file exposes, and send it the #detect message with a block comparing each line to a regular expression.

f = open('071-break-with-value.org')
f.lines.detect do |line|
  line =~ /banana/
end
    line =~ /banana/

Now let's add a further requirement. We only care if the line occurs in the first hundred lines of the file. If it doesn't, we don't want to waste time processing any more lines; we want to immediately stop and move on. To do this we can use a break in conjunction with the #lineno method on IO objects.

f = open('071-break-with-value.org')
f.lines.detect do |line|   
  break if f.lineno >= 100
  line =~ /banana/
end
nil

When we run this code the result is a nil, because the pattern wasn't found in the first 100 lines, and the break ended the search before it could go any further than that.

Now let's add one more requirement. We'd like this snippet of code to always return a string, no matter what. If it finds a matching line, it should return that line. If it doesn't, it should return an informational string indicating that a match wasn't found.

One way to do this would be to add an "OR" to the end of the #detect block, so that if nil is returned it is replaced with the informational string.

f = open('071-break-with-value.org')
f.lines.detect do |line|   
  break if f.lineno >= 100
  line =~ /banana/
end || "<Line Not Found>"
<Line Not Found>

But another way to do it, one that I find more readable, is to supply the placeholder string as an argument to break.

f = open('071-break-with-value.org')
f.lines.detect do |line|   
  break "<Line Not Found>" if f.lineno >= 100
  line =~ /banana/
end
<Line Not Found>

As before, the break overrides the original nil return when no line is matched.

This doesn't just read better. It also enables us to make use of block-local values when constructing the no-match-found result. For instance, we could include the line number and text of the last line searched in the failure message.

f = open('071-break-with-value.org')
f.lines.detect do |line|    
  if f.lineno >= 100
    break "<Not Found (Stopped at #{f.lineno}: '#{line.chomp}')>" 
  end
  line =~ /banana/
end
<Not Found (Stopped at 100: 'grab the =lines= enumerator that an open file exposes, and send it the')>

What we've seen today is that break can not only force a method to return early, it can also override the return value of the method in the process. It is a powerful tool when dealing with methods which yield to blocks, with many applications.

That's all for now. Happy hacking!

Responses