简体   繁体   中英

Ruby Block statements and Implicit Returns

I always thought that rubyists choose to make returns in ruby implicit because of a style preference (less words = more concise). However, can someone confirm with me that in the following example you actually have to make the returns implicit or else the intended functionality won't work? (The intended functionality is to be able to split a sentence into words and return either "Begins with a vowel" or "Begins with a consonant" for each word)

# With Implicit Returns
def begins_with_vowel_or_consonant(words)
  words_array = words.split(" ").map do |word|
    if "aeiou".include?(word[0,1])
      "Begins with a vowel" # => This is an implicit return
    else
      "Begins with a consonant" # => This is another implicit return
    end
  end
end

# With Explicit Returns
def begins_with_vowel_or_consonant(words)
  words_array = words.split(" ").map do |word|
    if "aeiou".include?(word[0,1])
      return "Begins with a vowel" # => This is an explicit return
    else
      return "Begins with a consonant" # => This is another explicit return
    end
  end
end

Now, I know there are definitely many ways to make this code more efficient and better, but the reason I've laid it out like this is to illustrate the need for the implicit returns. Can someone confirm with me that implicit returns are indeed needed and not just a stylistic choice?

EDIT: Here's an example to illustrate what I'm trying to show:

# Implicit Return
begins_with_vowel_or_consonant("hello world") # => ["Begins with a consonant", "Begins with a consonant"] 

# Explicit Return
begins_with_vowel_or_consonant("hello world") # => "Begins with a consonant" 

The implicit return value of a method is the last expression evaluated in the method.

In your case, neither of the two lines you annotated are the last expression. The last expression that gets evaluated is the assignment to words_array (which BTW is completely useless since because it is the last expression there is no way to use that variable afterwards).

Now, what is the value of an assignment expression? It is the value being assigned, in this particular case, the return value of the map method, which is an Array . So, that is what the method returns.

In the second example, at the very first iteration of the map , you will hit one of the two return s and thus immediately return from the method. In the first example, however, you will always iterate through the entire words array.

The problem is not that implicit and explicit returns are different, the problem is that the two lines you claim are implicit returns aren't.

This reason this is happening is because the return statement is inside of a block. If the return statement was inside of just a function, execution flow would be as you expect. Blocks and returns in ruby (and break statements for that matter) are a weird beast. Let's take a simplier example to capture what you're asking:

def no_return()
  (1..10).each do |i|
    puts i
  end
  puts 'end'
end

def yes_return()
  (1..10).each do |i|
    puts i
    return 
  end
  puts 'end'
end

If you run both of these, you'll see that the first will print out the numbers 1-10 and the word 'end' as you would expect, but the second function only prints out 1. This is because ruby internally implements returns (and break statements) as exceptions. When these exceptions are thrown, a lookup table called a "catch table" is used to try and find where the flow of execution should continue. If none is found, ruby internally will search down through the stacks of rb_control_frame structures looking for the pointer that points to the code that we are executing. So in your case, the exception is thrown and the closest pointer (in terms of stack frames) is at the end of the method, essentially causing a termination of the entire method. That's why you won't even see the 'end' being printed.

no_return instructions:

0000 trace            1                                               (   3)
0002 putnil           
0003 getdynamic       i, 0
0006 send             :puts, 1, nil, 8, <ic:0>
0012 leave    

yes_return instructions:

0000 trace            1                                               (   3)
0002 putnil           
0003 getdynamic       i, 0
0006 send             :puts, 1, nil, 8, <ic:0>
0012 pop              
0013 trace            1                                               (   4)
0015 putnil           
0016 throw            1
0018 leave 

The important bit is in the yes_return instructions, there's that 'throw 1' that is actually the return statement implemented as a thrown (and caught) exception. Breaks work in the same way except they 'throw 2'.

To see the actual instructions with all of the frames and catch table yourself, check out this app I put together not too long ago: http://rubybytes.herokuapp.com/

For more information, check out Pat Shaughnessy's blog, in particular this entry for more information: http://patshaughnessy.net/2012/6/29/how-ruby-executes-your-code

And another shameless plug, if you're into java, check out my java version of rubybytes at javabytes.herokuapp.com to see disassembled java bytecode (sorry for it being linkless, you'll have to copy and paste the url into your browser).

return将从产生该块的方法返回,因此您需要隐式或使用next

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM