Home > Software engineering >  yield is called once but gives two different results
yield is called once but gives two different results

Time:06-25

Sorry for a silly question. I'm reading a book to learn Ruby and there is a piece of code tha looks like that:

def make_casserole
  puts "Preheat oven to 375 degrees"
  ingredients = yield
  puts "Place #{ingredients} in dish"
  puts "Bake for 20 minutes"
end

make_casserole do
  "noodles, celery, and tuna"
end
make_casserole do
  "rice, broccoli, and chicken"
end

And the result you'll get:

Preheat oven to 375 degrees
Place noodles, celery, and tuna in dish
Bake for 20 minutes
Preheat oven to 375 degrees
Place rice, broccoli, and chicken in dish
Bake for 20 minutes

Question: Why the reslut looks as it is? They calling "yield" only once to get first one:

make_casserole do
  "noodles, celery, and tuna"
end

How does it gets the second? Shouldn't result be like this:

Preheat oven to 375 degrees
Place noodles, celery, and tuna in dish
Bake for 20 minutes

CodePudding user response:

yield yields control to a block. The return value of that block is what your method (in this case make_casserole) gets back from yield.

Consider if we have the block get input from the user:

irb(main):010:0> make_casserole do
irb(main):011:1*   puts "Ingredients: "
irb(main):012:1>   gets.strip
irb(main):013:1> end
Preheat oven to 375 degrees
Ingredients: 
cheese and beans
Place cheese and beans in dish
Bake for 20 minutes
=> nil

The examples you've shown work the way they do because the block just returns a string without doing anything else. Understanding how blocks work is crucial to reading and writing idiomatic Ruby code.

CodePudding user response:

Why Your Code Works As It Does

In Ruby, yield is a language keyword, but a number of objects also implement #yield or yield-like methods. Yield's job is to collaborate with a block. It might help to think of blocks as a sort of anonymous Proc object (which happens to have its own Proc#yield method) that is explicitly or implicitly available to every Ruby method as part of the language syntax.

The do/end keywords also delimit a block, so when you call:

make_casserole do
  "noodles, celery, and tuna"
end

this is exactly the same as if you'd used a block-literal such as:

make_casserole { "noodles, celery, and tuna" }

You're then assigning the result of calling the block back to ingredients, and since each call to #make_casserole is returning only a single String, each call can only provide a single result.

Some Alternatives

While you could theoretically make a block yield multiple values, this isn't particularly idiomatic and is really more appropriate for positional arguments. Consider the following:

def make_casserole *ingredients
  step = 0
  instruction = "%d. %s\n"
  puts [
    "Preheat oven to 375 degrees",
    "Place #{ingredients.join ', '} in dish",
    "Bake for 20 minutes"
  ].map { format instruction, step  = 1, _1 }
end

This does everything you currently do (and more), without needing a block at all. However, its behavior is largely hard-coded, which is where blocks can help.

When to Use Blocks

Blocks are most useful when you want to allow some unanticipated action to be defined or performed dynamically at runtime. For example, maybe you want to prepend an arbitrary word to each ingredient that you haven't predefined in your method or in the list of arguments you're passing:

# This method takes arguments, but yields each ingredient
# to the block for processing that wasn't defined by the
# current method.
def make_casserole *ingredients
  puts "Preheat oven to 375 degrees"
  puts ingredients.map { "Place #{yield _1} in dish" }
  puts "Bake for 20 minutes"
end

# Call the method with some ingredients as arguments,
# and then transform the ingredients according to the
# rules defined in the block.
make_casserole('beans', 'rice') do
  case _1
  when /beans|barley/; "organic #{_1}"
  when /rice|potatoes/; "brown #{_1}"
  else _1
  end
end

This could be done other ways, of course, but what it allows you to do is to defer decisions about how to act on the yielded items until runtime. The method doesn't really know (or care) what the block returns; it just yields each item in turn and lets the block act on the item. You might decide to pass a block that specifies substitutions, colors, levels of ripeness, or whatever else you want, all using the same method but using a different block.

By yielding each item to the block, the method's behavior can be modified on the fly based on the contents of the block. This creates a great deal of flexibility by leaving certain implementation details up to the block rather than the method.

  •  Tags:  
  • ruby
  • Related