Skip to content

Instantly share code, notes, and snippets.

@JEG2

JEG2/README.md Secret

Created October 11, 2011 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JEG2/3432cd18bf67aef4c14b to your computer and use it in GitHub Desktop.
Save JEG2/3432cd18bf67aef4c14b to your computer and use it in GitHub Desktop.
An entry for the "multiple blocks" CodeBrawl

MultiBlock

Ruby's syntax doesn't easily allow for passing multiple blocks to a method. This can sometimes make for awkward interfaces. Ruby's find() iterator has this problem when you choose to pass a block for handling the "not found" condition.

MultiBlock solves this problem by allowing you to pass multiple blocks to a method by name. Here's the find() example rewritten using MultiBlock:

require "multi_block"

module Enumerable
  def multi_block_find(blocks)
    find(blocks.if_not_found, &blocks.test)
  end
  multi_block :multi_block_find, :test, :if_not_found
end

enum = [1, 2, 3]

# Ruby's find()
p enum.find(&:even?)
p enum.find { |n| n > 3 }
begin
  enum.find(-> { fail "Not found" }) { |n| n > 3 }
rescue => error
  puts error.message
end

# using MultiBlock
begin
  enum.multi_block_find do |blocks|
    blocks.test         { |n| n > 3            }
    blocks.if_not_found {     fail "Not found" }
  end
rescue => error
  puts error.message
end

Usage

As you can see above, you just call multi_block() with the name of the method you want to take multiple blocks. You can optionally pass the block names to accept or just leave them out to allow for any block name. The blocks will be passed in a single argument added to the end of the arguments list. Blocks are accessed by calling a method named after the block.

You can call multi_block() on instance methods, mix-in methods, top-level methods, and class/module methods through singleton_class():

module SomeModule
  def self.some_module_method
    # ...
  end
  singleton_class.multi_block :some_module_method
end

License

This code is in the Public Domain. You are free to use it in any way that you like.

class MultiBlock < BasicObject
def initialize(*block_names)
@block_names = block_names
@blocks = { }
end
def respond_to?(method, include_private = false)
if @block_names.empty? or @block_names.include? method
true
else
super
end
end
def method_missing(method, *_, &block)
if @block_names.empty? or @block_names.include? method
@blocks[method] = block if block
@blocks[method]
else
super
end
end
end
module MultiBlockable
def multi_block(method_name, *block_names)
if is_a? Module
target = self
method = instance_method(method_name)
else
target = Object
method = method(method_name)
end
target.send(:define_method, method_name) do |*args, &block_definitions|
blocks = MultiBlock.new(*block_names)
if block_definitions
if block_definitions.arity == 1
block_definitions.(blocks)
else
blocks.instance_eval(&block_definitions)
end
end
bound_method = method.is_a?(UnboundMethod) ? method.bind(self) : method
bound_method.(*args, blocks)
end
end
end
class Object; include MultiBlockable end
class Module; include MultiBlockable end
@jeremyevans
Copy link

Only complaint is that this requires you to define the method using some special metaprogramming. Also, a minor nit is that I'm not sure why you need @block_names when you have @blocks in MultiBlock.

@JEG2
Copy link
Author

JEG2 commented Oct 17, 2011

I wish it didn't ask us to rate our own entries. :)

@JEG2
Copy link
Author

JEG2 commented Oct 17, 2011

Block names allows the person who invokes the method rewriter to specify which blocks are permitted.

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