secret
Last active

Ruby Methods with Multiple Blocks

  • Download Gist
README.md
Markdown

Ruby Methods with Multiple Blocks

Ruby blocks rock. They're so great, sometimes we want to pass multiple blocks to a method, but, Ruby doesn't allow this. Consider the classic example where we want to perform a block of code if an action succeeds and perform different code if an action fails.

Fortunately, Ruby provides us the ability to modify itself. So with a couple lines of code we can achieve the above situation. In this case, let's add a perform method to the Proc class. This method will simply yield an anonymous Class with methods defined to handle our blocks.

class Proc
  def perform(callable)
    self === Class.new do
      method_name = callable.to_sym
      define_method(method_name) { |&block| block.nil? ? true : block.call }
      define_method("#{method_name}?") { true }
      def method_missing(method_name, *args, &block) false; end
    end.new
  end
end

That's it!

Example Usage:

Let’s try it with something useful. Let’s say we’re writing something which needs to happen in an all-or-nothing, atomic fashion. Either the whole thing works, or none of it does. A simple case is tweeting:

def tweet(message, &block)
  if Twitter.update(message)
    block.perform :success
  else
    block.perform :failure
  end
end

We call perform on the block and give it a name. Any name will work :success, :error, :fail!, etc. Now we can provide a status if the tweet was successful or not.

tweet "Ruby methods with multiple blocks. #lolruby" do |on|
  on.success do
    puts "Tweet successful!"
  end
  on.failure do
    puts "Sorry, something went wrong."
  end
end

Output:

Tweet successful!

The nice thing here is that we can define our own DSL. We don't need to worry about making sure passing too many or unexpected blocks. We could have easily said where.success or on.error or update.fail!.

Bonus: In addition to wrapping code in blocks, our Proc#perform method defines boolean style methods. So we could have call the tweet method like this if we wanted to:

tweet "Ruby methods with multiple blocks. #lolruby" do |update|
  puts "Tweet successful!" if update.success?
  puts "Sorry, something went wrong." if update.failure?
end
proc.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Proc
# Defines methods in an anonymous Class based on the given object.
# The first define_method will simply call the block if a block exists,
# otherwise true is returned. Undefined methods always return false.
#
# Returns an anonymous Class
def perform(callable)
self === Class.new do
method_name = callable.to_sym
define_method(method_name) { |&block| block.nil? ? true : block.call }
define_method("#{method_name}?") { true }
def method_missing(method_name, *args, &block) false; end
end.new
end
end
proc_test.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
require 'minitest/autorun'
require File.join(File.dirname(__FILE__), 'proc.rb')
 
class Sample
def save(condition = nil, &block)
if condition
block.perform :success
else
block.perform :failure
end
end
end
 
describe Sample do
 
before do
@sample = Sample.new
end
 
it 'calls the success block when condition is true' do
@sample.save(true) do |on|
on.success{ 'success!' }.must_equal 'success!'
on.failure{ 'failure!' }.must_equal false
end
end
 
it 'calls the failure block when condition is false' do
@sample.save(false) do |on|
on.success { 'success!' }.must_equal false
on.failure { 'failure!' }.must_equal 'failure!'
end
end
 
it 'calls the callback without any block' do
@sample.save(true) do |on|
on.success.must_equal true
on.failure.must_equal false
end
end
 
it 'calls unknown block' do
@sample.save(true) do |on|
on.error { 'error' }.must_equal false
end
end
 
it 'returns true from success? if condition is true' do
@sample.save(true) do |save|
save.success?.must_equal true
save.failure?.must_equal false
end
end
 
it 'returns false from success? if condition is false' do
@sample.save(false) do |save|
save.success?.must_equal false
save.failure?.must_equal true
end
end
 
end

I like this mini DSL!

I like the calling semantics a lot. Though I'm not a fan of monkey patching Ruby.

Love the simplicity of this!

This is my favorite solution. I had to play with the code a little to fully understand how it works and I'm very glad I did.

Huh. Didn't realize that === would invoke the passed in Proc though I suppose it makes sense. Clever way to call. Am I wrong that

self === Class.new do
  ...
end.new

could be replaced with:

Class.new do
  ...
end.new.call

?

That is, the use of comparison to self seems distracting from the fact that the anonymous subclass is instantiated and called when all is said and done.

A lot like my solution, just simpler with more monkey patching. I like this one the most, too. Damn, I hoped to beat Matt ;-)

I like the simplicity of the implementation, but it looks like you can't pass arguments to the blocks.

Good point, Jeremy.

On Monday, October 17, 2011 at 5:01 PM, Jeremy Evans wrote:

I like the simplicity of the implementation, but it looks like you can't pass arguments to the blocks.

Reply to this email directly or view it on GitHub:
https://gist.github.com/376e6db6e7773f9a6a84

It's easy to add, though. This, too, shall pass (arguments)

class Proc
  # Defines methods in an anonymous Class based on the given object.
  # The first define_method will simply call the block if a block exists,
  # otherwise true is returned. Undefined methods always return false.
  #
  # Returns an anonymous Class
  def perform(callable, *args)
    self === Class.new do
      method_name = callable.to_sym
      define_method(method_name) { |&block| block.nil? ? true : block.call(*args) }
      define_method("#{method_name}?") { true }
      def method_missing(method_name, *args, &block) false; end
    end.new
  end
end

Usage on above argument passing update, for those that might not fully grasp the elegance of the solution, or blocks in general:

Assuming

def tweet(message, &block)
  if Twitter.update(message)
    block.perform :success, 'Tweet Successful!', 'Congrats' #, ...
  else
    block.perform :failure, 'Error data perhaps?' #, ...
  end
end

Then the following could be rewritten as..

tweet "Ruby methods with multiple blocks. #lolruby" do |on|
  on.success do |message, extra|
    puts message # Tweet Successful!
    # Something with extra
  end
  on.failure do |why|
    puts why # Error data perhaps?
  end
end

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.