Skip to content

Instantly share code, notes, and snippets.

@boffbowsh
Created October 10, 2011 21:07
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 boffbowsh/c33d4e4a9e216677ffdc to your computer and use it in GitHub Desktop.
Save boffbowsh/c33d4e4a9e216677ffdc to your computer and use it in GitHub Desktop.
MMMultiYield

MMMultiYield

MMMultiYield allows your Ruby methods to take multiple named blocks with a simple DSL and yield to them independently in code. This allows for things such as callback and errback blocks, flexible case-like patterns and other stuff I've not thought of.

Requirements

Ruby 1.9.2 (1.8.7 and Rubinius's implementation of Kernel#caller doesn't show the class if you're called from a class body) JRuby in the future

How to use

Defining a method

To define a method within current scope (e.g. within a class):

class Foobar
  MMMulti.def :my_new_method do |mmmulti, my_arg|
    if my_arg == 'blue'
      mmmulti.yield(:callback)
    else
      mmmulti.yield(:errback)
    end
  end
end

To define it on an explicit class:

MMMulti.def(String => :my_string_method) do |mmmulti|
  if self == 'blue'
    mmmulti.yield(:callback)
  else
    mmmulti.yield(:errback)
  end
end

Invocation

>> Foobar.new.my_new_method 'blue' do
  def callback
    'omg it r blue'
  end
    
  def errback
    ':((( no blu'
  end
end
=> "omg it r blue"

>> 'green'.my_string_method do
  def callback
    'omg it r blue'
  end
    
  def errback
    ':((( no blu'
  end
end
=> ":((( no blu"

If the method tries to yield to a block that isn't named in the block-of-blocks, it will raise NoMethodError

Notes

I haven't used BlankSlate for the DSL because HOLY JESUS THAT THING IS SLOW. This is mainly a toy, so perhaps avoid naming your blocks after methods you shouldn't.

Minitest is used for the spec. Enjoy!

class MMMulti
class << self
def def name, &block
if name.is_a? Hash
obj = name.keys[0]
name = name.values[0]
else
obj = object_to_define_on(caller)
end
obj.send :raise, LocalJumpError unless block_given?
obj.send :define_method, name do |*args, &block_block|
mmmulti = MMMulti.new(block_block)
block.call mmmulti, *args
end
end
private
def object_to_define_on kaller, instance = true
/^(.+?):(\d+)(?::in `(.*)')?/ =~ kaller.shift
case $3
when '<main>'
Object
when 'singletonclass'
object_to_define_on kaller, false
when /^<class:(.*)>$/
klass = object_to_define_on(kaller).const_get($1)
klass = klass.class_eval('class << self; self; end') unless instance
klass
when /^<module:(.*)>$/
object_to_define_on(kaller).const_get($1)
end
end
end
def initialize block_block
klass = Class.new
klass.module_exec(&block_block)
@block_block = klass.new
end
def yield name, *args, &block
@block_block.send name, *args, &block
end
end
require File.expand_path('mmmulti_yield')
require 'minitest/autorun'
module Foo
MMMulti.def :module_method do |mmmulti, arg|
arg ? mmmulti.yield(:callback) : mmmulti.yield(:errback)
end
end
class Bar
MMMulti.def :instance_method do |mmmulti, arg|
arg ? mmmulti.yield(:callback) : mmmulti.yield(:errback)
end
class << self
MMMulti.def :class_method do |mmmulti, arg|
arg ? mmmulti.yield(:callback) : mmmulti.yield(:errback)
end
end
end
module Moo
class Oink
MMMulti.def :namespaced_instance_method do |mmmulti, arg|
arg ? mmmulti.yield(:callback) : mmmulti.yield(:errback)
end
end
end
MMMulti.def 'main_method' do |mmmulti, arg|
arg ? mmmulti.yield(:callback) : mmmulti.yield(:errback)
end
MMMulti.def({String => :string_method}) do |mmmulti, arg|
arg ? mmmulti.yield(:callback) : mmmulti.yield(:errback)
end
describe MMMulti do
before do
$result = nil
@block = Proc.new {
def callback
$result = :ok
end
def errback
$result = :error
end
}
end
it "should work on <main>" do
main_method true, &@block
$result.must_equal :ok
end
it "should work on a module" do
Class.new.tap {|c| c.send :include, Foo }.new.module_method false, &@block
$result.must_equal :error
end
it "should work on an instance" do
Bar.new.instance_method true, &@block
$result.must_equal :ok
end
it "should work on a class" do
Bar.class_method true, &@block
$result.must_equal :ok
end
it "should work on a namespaced class" do
Moo::Oink.new.namespaced_instance_method true, &@block
$result.must_equal :ok
end
it "should work on an explicit receiver" do
'test'.string_method true, &@block
$result.must_equal :ok
end
it "should raise no method error on an unhandled callback" do
lambda { main_method true do
def foobar
'moooink'
end
end }.must_raise NoMethodError
end
end
@jeremyevans
Copy link

Operating on the caller array to determine where to define the method feels wrong to me.

@JEG2
Copy link

JEG2 commented Oct 17, 2011

I miss not being able to use the normal def, but this seems fine otherwise.

@Keoven
Copy link

Keoven commented Oct 17, 2011

When coming up with my own solution, I considered doing something like this. I think I might have dropped the idea because it won't be as clean as defining a normal method. Then again, this solution works for me.. :)

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