When blocks are more useful than functions (ruby)?
-
16-12-2019 - |
Question
I have two examples that give the same result.
With block:
def self.do_something(object_id)
self.with_params(object_id) do |params|
some_stuff(params)
end
end
def self.with_params(object_id, &block)
find_object_by_id
calculate_params_hash
block.call(params_hash)
end
and with method:
def self.do_something(object_id)
some_stuff(self.get_params(object_id))
end
def self.get_params(object_id)
find_object_by_id
calculate_params_hash
params_hash
end
The second solution seems more straightforward, but I found some usages of the first one in our application code. My question is: in which situation the first one is recommended? What are the pros and cons of each one?
Solution
The main difference between a block and function as per your example is that the block runs within the context of the calling function.
So if your example was as:
def self.do_something(object_id)
x = "boogy on"
self.with_params(object_id) do |params|
some_stuff(params)
puts x
end
end
The code within the block can access the variable x that was defined outside the block. This is called a closure. You couldn't do this if you were just calling a function as per your second example.
Another interesting thing about blocks is they can affect the control flow of the outer function. So it is possible to do :
def self.do_something(object_id)
self.with_params(object_id) do |params|
if some_stuff(params)
return
end
end
# This wont get printed if some_stuff returns true.
puts "porkleworkle"
end
If the some_stuff call within the block returns a true value, the block will return. This will return out of the block and out of the dosomething method. porkleworkle would not get output.
In your examples you don't rely on either of these, so using function calls is probably much cleaner.
However, there are many situations where using blocks to allow you to take advantage of these things is invaluable.
OTHER TIPS
Normally people use blocks when they want to run a piece of code inside of another piece of code. Examples:
DB.with_shard_for_user(user_id) do |db|
# perform operations on a user's shard
end # shard is reverted back to original value
File.new(filename) do |file|
# work with file
end # file is closed automatically
User.transaction do
# run some operations as a single transaction
end
These blocks are closed on their lexical context (they capture variables from where the block is declared, and carry them over to the place when blocks are called).
Schematic structure of a method that accepts a block.
def transaction
open_transaction # pre- part
yield if block_given? # run provided code
commit_transaction # post- part
rescue
rollback_transaction # handle problems
end
In your first example, use of a block is probably unjustified (IMHO). Too complex for no apparent reason.
When you call with_params(), you are not only sending in data, you are also providing some code to run. See if different blocks are sent into the with_params() call:
...
self.with_params(object_id) do |params|
some_other_stuff()
some_stuff(params)
end
...
and somewhere else:
...
self.with_params(object_id) do |params|
even_more_stuff(params)
end
...
If the blocks are all the same or with_params() is just called from one place then you might consider eliminating the blocks.
To sum up: use blocks if you want to pass into the method different bits of code(blocks) as well as data: hey with_params, take this data(object_id) and, by the way, run this code(block) while you're at it.
BTW you are doing different things in the two examples: with_params() returns
some_stuff(params_hash)
after evaluating the block. And get_params() just returns
params_hash
A block totally rely on your code, but a function has its own code.
So, if your code varies situation by situation, use block. If not, build a function and use it as a block box.