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