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.