Skip to content

Instantly share code, notes, and snippets.

@mattsears
Created October 17, 2011 00:34
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mattsears/376e6db6e7773f9a6a84 to your computer and use it in GitHub Desktop.
Save mattsears/376e6db6e7773f9a6a84 to your computer and use it in GitHub Desktop.
Ruby Methods with Multiple Blocks

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
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
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
@asaaki
Copy link

asaaki commented Oct 17, 2011

I like this mini DSL!

@elight
Copy link

elight commented Oct 17, 2011

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

@eric1234
Copy link

Love the simplicity of this!

@JEG2
Copy link

JEG2 commented Oct 17, 2011

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.

@elight
Copy link

elight commented Oct 17, 2011

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

?

@elight
Copy link

elight commented Oct 17, 2011

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.

@rogerbraun
Copy link

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

@jeremyevans
Copy link

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

@elight
Copy link

elight commented Oct 17, 2011 via email

@rogerbraun
Copy link

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

@adizam
Copy link

adizam commented Mar 24, 2014

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment