Skip to content

Instantly share code, notes, and snippets.

@janlelis
Created October 13, 2011 13:30
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save janlelis/4b2f5fd0b45118e46d0f to your computer and use it in GitHub Desktop.
Save janlelis/4b2f5fd0b45118e46d0f to your computer and use it in GitHub Desktop.
NamedProc & MultiBlock

MultiBlock

MultiBlock is a mini framework for passing multiple blocks to methods. It uses “named procs” to accomplish this in a nice way. The receiving method can either yield all blocks, or just call specific ones, identified by order or name.

These gem was build during a codebrawl contest. You might also take a look at the other entries: codebrawl.com/contests/methods-taking-multiple-blocks

Setup

gem install multi_block

Named Procs

A named proc acts like a normal proc, but has got a name attribute. You can create it by calling a method with the desired name on proc:

>> a = proc.even?{ |e| e.even? }
=> #<NamedProc:0x00000001ffc340@(irb):1>
>> a.name
=> :even?
>> a[42]
=> false

In the same way, you can create lambdas:

>> b = lambda.doubler{ |e| e * 2 }
=> #<NamedProc:0x000000020685e0@(irb):7 (lambda)>
>> b.name
=> :doubler
>> b[21]
=> 42
>> b.lambda?
=> true

MultiBlock Usage

Defining methods that use multiple blocks

The first argument given to yield always defines the desired block(s). The other arguments get directly passed to the block(s). So these are example calls to the block:

yield                                            # calls all given procs without args
yield :success                                   # calls :success proc without args
yield :success, "Code Brawl!"                    # calls :success proc with message
yield 1                                          # calls first proc (:success in this case)
yield [:success, :bonus]                         # calls :success and :bonus without args
yield [:success, :bonus], "Code Brawl!"          # calls both procs with same arg
yield success: "Code Brawl!",                    # calls each keyed proc,
      error:   [500, "Internal Brawl Error"]     #       values are the args

Consider these two example methods:

def ajax
  yield rand(6) != 0 ? :success : :error # calls the :success block if everything worked well
end

def dice
  random_number = rand(6)
  yield random_number, random_number + 1 # calls the n-th block
end

Calling methods with multiple blocks

It’s done by calling the blocks helper method:

ajax &blocks[
  proc.success do puts "Yeah!" end,
  proc.error   do puts "Error..." end,
]

The dice method could, for example, be called in this way:

dice &blocks[
  proc{ ":(" },
  proc{ ":/" },
  proc{ ":O" },
  proc{ ":b" },
  proc{ ":P" },
  proc{ rand(42) != 0 ? ":)"  : ":D"},
]

Bonus sugar: Array extension

If you like the slim &to_proc operator, you can further optimize the syntax by calling:

Array.send :include, MultiBlock::Array

Now, it’s getting real hot:

do_something, some_argument, &[
  proc.easy_way do
    # do it the easy way
  end,

  proc.complex_way do
    # use complex heuristics, etc.
  end,
]

J-_-L

The MIT LICENSE
Copyright (c) 2011 Jan Lelis
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# -*- encoding: utf-8 -*-
require 'rubygems' unless defined? Gem
Gem::Specification.new do |s|
s.name = "multi_block"
s.version = 1.0
s.authors = ["Jan Lelis"]
s.email = "mail@janlelis.de"
s.homepage = "https://gist.github.com/4b2f5fd0b45118e46d0f"
s.summary = 'MultiBlock is a mini framework for passing multiple blocks to methods.'
s.description = 'MultiBlock is a mini framework for passing multiple blocks to methods. It uses "named procs" to accomplish this in a nice way. The receiving method can either yield all blocks, or just call specific ones, identified by order or name. '
s.required_ruby_version = '>= 1.9.2'
s.files = Dir.glob %w{multi_block.gemspec lib/multi_block.rb spec/multi_block_spec.rb}
s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"]
s.license = 'MIT'
s.add_dependency 'named_proc'
s.add_development_dependency 'rspec'
s.add_development_dependency 'rspec-core'
end
# encoding: utf-8
require 'named_proc'
module MultiBlock
# multiple block transformation method,
# sorry for the method length and the code dup ;)
def self.[](*proc_array)
# Create hash representation, proc_array will still get used sometimes
proc_hash = {}
proc_array.each{ |proc|
proc_hash[proc.name] = proc if proc.respond_to?(:name)
}
# Build yielder proc
Proc.new{ |*proc_names_and_args|
if proc_names_and_args.empty? # call all procs
ret = proc_array.map(&:call)
proc_array.size == 1 ? ret.first : ret
else
proc_names, *proc_args = *proc_names_and_args
if proc_names.is_a? Hash # keys: proc_names, values: args
proc_names.map{ |proc_name, proc_args|
proc = proc_name.is_a?(Integer) ? proc_array[proc_name] : proc_hash[proc_name.to_sym]
proc or raise LocalJumpError, "wrong block name given (#{proc_name})"
[proc, Array(proc_args)]
}.map{ |proc, proc_args|
proc.call(*proc_args)
}
else
ret = Array(proc_names).map{ |proc_name|
proc = proc_name.is_a?(Integer) ? proc_array[proc_name] : proc_hash[proc_name.to_sym]
proc or raise LocalJumpError, "wrong block name given (#{proc_name})"
[proc, Array(proc_args)]
}.map{ |proc, proc_args|
proc.call(*proc_args)
}
ret.size == 1 ? ret.first : ret
end
end
}
end
# low level mixins
module Object
private
# to_proc helper, see README
def blocks
MultiBlock#[]
end
# alias procs blocks
# alias b blocks
end
::Object.send :include, ::MultiBlock::Object
# Bonus array mixin (if you want to)
module Array
# see README for an example
def to_proc
::MultiBlock[*self]
end
end
# ::Array.send :include, MultiBlock::Array
end
# encoding: utf-8
require_relative '../lib/multi_block'
describe "blocks" do
it "returns the MutliBlock constant (for calling [] on it)" do
blocks.should == MultiBlock
end
end
describe MultiBlock, "#[]" do
it "yield without args: calls every block and returns array of results" do
def null
yield
end
null(&blocks[
proc{5},
proc{6},
]).should == [5,6]
end
it "yield with symbol: calls the specified proc, other args get passed" do
def symbol
yield :success, "Code Brawl!"
end
symbol(&blocks[
proc{5},
proc.success{|e| e.swapcase},
proc.error{6},
]).should == "cODE bRAWL!"
end
it 'yield with symbol: raises LocalJumpError if proc name is wrong' do
def wrong_name
yield :wrong, "Code Brawl!"
end
proc do
wrong_name(&blocks[
proc{5},
proc.success{|e| e.swapcase},
proc.error{6},
])
end.should raise_exception(LocalJumpError)
end
it "yield with integer: calls the n-th proc, other args get passed" do
def integer
yield 2
end
integer(&blocks[
proc{5},
proc.success{|e| e.swapcase},
proc.error{6},
]).should == 6
end
it "yield with array: calls all procs, indentified by symbol or integer, other args get passed" do
def array
yield [:success, :error], "Code Brawl!"
end
array(&blocks[
proc{5},
proc.success{|e| e.swapcase},
proc.error{|e| e.downcase},
]).should == ["cODE bRAWL!", "code brawl!"]
end
it "yield with hash: takes keys as proc names and passes values as proc args" do
def hash
yield success: "Code Brawl!", error: [500, "Internal Brawl Error"]
end
hash(&blocks[
proc{5},
proc.success{|e| e.swapcase},
proc.error{|no, msg| "Error #{no}: #{msg}"},
]).sort.should == ["Error 500: Internal Brawl Error", "cODE bRAWL!"]
end
end
# -*- encoding: utf-8 -*-
require 'rubygems' unless defined? Gem
Gem::Specification.new do |s|
s.name = "named_proc"
s.version = 1.0
s.authors = ["Jan Lelis"]
s.email = "mail@janlelis.de"
s.homepage = "https://gist.github.com/4b2f5fd0b45118e46d0f"
s.summary = "NamedProc: Like anonymous procs, but have a name."
s.description = "NamedProc: Like anonymous procs, but have a name. Example: lambda.codebrawl {} # creates an empty lambda with the name :codebrawl"
s.required_ruby_version = '>= 1.9.2'
s.files = Dir.glob %w{named_proc.gemspec lib/named_proc.rb spec/named_proc_spec.rb}
s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"]
s.license = 'MIT'
s.add_development_dependency 'rspec'
s.add_development_dependency 'rspec-core'
end
# encoding: utf-8
# Class for a proc that's got a name
class NamedProc < Proc
attr_reader :name
def initialize(name)
@name = name
super
end
# create one from a given proc/lambda object
def self.create(name, block, lambda = false)
name = name.to_sym
# sorry for this ugly hack, is there a better way to lambdafy?
block = Module.new.send(:define_method, name.to_sym, &block) if lambda
new(name, &block)
end
# Proxy object to ease named proc initialization
module Proxy
Proc = BasicObject.new
def Proc.method_missing(name, &block) NamedProc.create(name, block) end
Lambda = BasicObject.new
def Lambda.method_missing(name, &block) NamedProc.create(name, block, true) end
end
# Mixing in low level method "links"
module Object
private
# create a proc with name if given
def proc
if block_given?
super
else
NamedProc::Proxy::Proc
end
end
# same for lambda
def lambda
if block_given?
super
else
NamedProc::Proxy::Lambda
end
end
end
::Object.send :include, NamedProc::Object
end
# encoding: utf-8
require_relative '../lib/named_proc'
describe "proc" do
it "creates a new proc as usual when called with a block" do
a = proc{}
a.should be_instance_of Proc
a.lambda?.should == false
end
it "creates a named proc when a method gets called on it" do
a = proc.brawl{}
a.should be_a Proc
a.should be_instance_of NamedProc
a.lambda?.should == false
a.name == :brawl
end
end
describe "lambda" do
it "creates a new lambda as usual when called with a block" do
a = lambda{}
a.should be_instance_of Proc
a.lambda?.should == true
end
it "creates a named lambda when a method gets called on it" do
a = lambda.brawl{}
a.should be_a Proc
a.should be_instance_of NamedProc
a.lambda?.should == true
a.name == :brawl
end
end
@ericgj
Copy link

ericgj commented Oct 17, 2011

Excellent

@asaaki
Copy link

asaaki commented Oct 17, 2011

Nice approach to avoid the origin behavior of method_missing in Proc or other classes/modules.
And extra bonus point for including lambdas.

@JEG2
Copy link

JEG2 commented Oct 17, 2011

This was an interesting syntax twist using your proxy object. Nice.

@asaaki
Copy link

asaaki commented Oct 29, 2011

This Gist contains subdirectories, which are not supported. You may want to create a repository in GitHub.

Uh oh?

@janlelis
Copy link
Author

I've released it as gems (named_proc & multi_block), but don't want to put it in an extra repo, unless there's much updating going on

@asaaki
Copy link

asaaki commented Oct 29, 2011

but don't want to put it in an extra repo, unless there's much updating going on

It doesn't matter how often it will be updated, but would be helpful for others to contribute if they want to. ;o)

And I like the repo code view much more if their are more than one file.

Don't be afraid to share your work more publicly. It is the fifth place, that's honorable! Hooray!

@janlelis
Copy link
Author

janlelis commented Nov 1, 2011

OK, you are right ;), I'll move it to github, the next time I update it.

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