Skip to content

Instantly share code, notes, and snippets.

@hawx

hawx/README.md Secret

Created October 10, 2011 17:26
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 hawx/ca4e697b65d9b91ef531 to your computer and use it in GitHub Desktop.
Save hawx/ca4e697b65d9b91ef531 to your computer and use it in GitHub Desktop.
Create chains of procs to simulate passing multiple blocks to a method.

ProcArrays, or How to Kind of Pass Multiple Blocks to a Method

Wait ProcArrays, you mean BlockArrays, well no. Ruby doesn't like it when you try to call a method on a block (eg. my_method {|f| ... }.something(1)) see EOF. so instead the first thing must be a Proc, then you can chain blocks on to that as it is OK to call methods on a Proc. So here is an example:

def call_them(procs)
  procs.call
end

# Note I used the `->` proc style because it is nicer, `proc` would still work.
call_them ->{ "First" }.and { "Second" }
#=> ["First", "Second"]

So, above I passed two blocks/procs (proc with a block really) to a method and called them. The results of calling each proc were then returned in an Array. But we can do more interesting things than just call the Procs in the ProcArray. We can pass them arguments as well:

def call_them(procs)
  procs.call(['dogs'], ['like', 'cats'])
end

call_them ->(noun){ puts noun }.and {|verb, noun| puts "#{verb} #{noun}" }
#=> "dogs"
#=> "like cats"

See that I passed the list of arguments for each Proc as an Array, allowing me to pass two arguments to the last Proc/block.

Checking the Number of Procs Passed

ProcArray responds to #size and I've also monkey patched Proc to as well, so it doesn't matter if a single Proc or a massive ProcArray is passed you can check it's #size.

def two_to_four_procs(procs)
  if procs.size < 2
    raise '#two_to_four_procs expects at least two procs'
  elsif procs.size > 4
    raise '#two_to_four_procs expects at most four procs'
  else
    # do something
  end
end

It's Just an Array

Remember ProcArray is a subclass of Array so you can use normal array methods as expected.

procs = ->(i){ i * 2 }.and {|i| i * 3 }.and {|i| i * 4 }

procs.inject(2) {|a,e| e.call(a) }
#=> 48      # ((2 * 2) * 3) * 4

procs.map {|i| i.call(2) }
#=> [4, 6, 8]

  # OR
  procs.call([2], [2], [2])
  #=> [4, 6, 8]

  # OR
  procs.call *([2] * 3)
  #=> [4, 6, 8]

procs.find_all {|i| i.call(1) % 2 == 0 }
#=> [->(i){ i * 2 }, ->(i){ i * 4 }]

procs.each do |prc|
  puts prc.call(5)
end
#=> 10
#=> 15
#=> 20

two, three, four = procs
two[50]
#=> 100
three[100]
#=> 300
four[500]
#=> 2000

Appendix: Parsing Issues

If you attempt

def m(&block)
  block.call
end

m { puts 'First' }.and { puts 'Second' }
# NoMethodError: undefined method `and' for nil:NilClass
#	from (irb):11
#	from /Users/Josh/.rbenv/versions/1.9.2-p290/bin/irb:12:in `<main>'

It raises a NoMethodError because for some reason it tries to call #and on the return value of the calling the first block. This is I think due to ruby on seeing a block parsing it in a special way as (kind of) shown below.

require 'ripper'

Ripper.sexp "method {}.and {}"
#=> [:program,
     [[:method_add_block,
       [:call,
        [:method_add_block,
         [:method_add_arg, [:fcall, [:@ident, "method", [1, 0]]], []],
         [:brace_block, nil, [[:void_stmt]]]],
        :".",
        [:@ident, "and", [1, 10]]],
       [:brace_block, nil, [[:void_stmt]]]]]]

Ripper.sexp "method ->{}.and {}"
#=> [:program,
     [[:command,
       [:@ident, "method", [1, 0]],
       [:args_add_block,
        [[:method_add_block,
          [:call,
           [:lambda, [:params, nil, nil, nil, nil, nil], [[:void_stmt]]],
           :".",
           [:@ident, "and", [1, 13]]],
          [:brace_block, nil, [[:void_stmt]]]]],
        false]]]]
class ProcArray < Array
# Once a ProcArray has been created by Proc#and this method is
# called to chain even more.
def and(&block)
raise 'Need to give #and a block' unless block_given?
self << block
self
end
# The arguments for each proc should be passed in as an Array.
#
# @example
#
# multi = ->{ puts 'Hi' }.
# and {|i| puts "You ordered #{i} beans" }.
# and { puts "Magic beans of course" }
#
# multi.call(nil, [5])
# #=> "Hi"
# #=> "You ordered 5 beans"
# #=> "Magic beans of course"
#
# @return [Array] The results from each Proc being called.
def call(*args)
map.with_index do |prc, i|
prc.call *args[i]
end
end
end
class Proc
# Creates a new ProcArray containing this and the block passed and
# any block chained with #and to that one.
#
# @example
#
# chain = ->{ 1 }.and { 2 }.and { 3 }
# chain.each do |prc|
# puts prc.call
# end
#
# @return [ProcArray]
def and(&block)
raise 'Need to give #and a block' unless block_given?
m = ProcArray.new
m << self
m << block
m
end
# This allows you to test the size of the objects passed whether it
# is a ProcArray (which already responds to #size) or a single Proc,
# then act on it to test for missing arguments or w/e.
#
# @example
#
# def some_method(some_procs)
# puts "I was passed #{some_procs.size}"
# if some_procs.size < 2
# puts "I need 2 or more procs"
# exit
# end
# end
#
def size
1
end
alias_method :length, :size
end
require_relative 'proc_array'
gem 'minitest'
require 'minitest/spec'
require 'minitest/autorun'
describe Proc do
describe "#and" do
it "returns a ProcArray" do
a = ->{}.and {}
a.must_be_kind_of ProcArray
end
it "contains the proc itself" do
a = ->{}
b = a.dup # dup because it gets modified
a = a.and {}
a.must_include b
end
it "contains the block passed" do
a = ->{}
b = ->{}
a.and(&b).must_include b
end
it "raises an error if block not passed" do
proc {
->{}.and
}.must_raise RuntimeError
end
end
describe "#size" do
it "returns 1" do
->{}.size.must_equal 1
->{}.length.must_equal 1
end
end
end
describe ProcArray do
describe "#and" do
before do
@procs = ProcArray.new
end
it "adds the block to the list" do
a = ->{}
@procs.and(&a).must_include(a)
end
it "raises an error if block not passed" do
proc {
->{}.and
}.must_raise RuntimeError
end
end
describe "#call" do
it "calls each proc in turn passing the correct arguments" do
a = ""
procs = ->{ a += "Hi\n" }.
and {|i| a += "You ordered #{i} beans\n" }.
and { a += "Magic beans of course" }
procs.call(nil, [5])
a.must_equal "Hi\nYou ordered 5 beans\nMagic beans of course"
end
it "returns an array of the results" do
procs = ->{ 2 }.and {|i| i * 3 }.and { 4 }
procs.call(nil, [5]).must_equal [2, 15, 4]
end
end
describe "#map, as an example of normal Enumerable behaviour" do
it "maps the procs" do
procs = ->i{ i * 3 }.and {|i| i * 6 }.and {|i| i * 9 }
procs.map {|prc| prc.call(2) }.must_equal [6, 12, 18]
end
end
describe "#size" do
it "returns the number of procs" do
->{}.and {}.size.must_equal 2
end
end
describe "#to_a" do
it "returns a regular array" do
ProcArray.new.wont_be_instance_of Array
ProcArray.new.to_a.must_be_instance_of Array
end
end
end
@elight
Copy link

elight commented Oct 17, 2011

Prefer to avoid reopening Proc. Like the chaining-based approach; however, it doesn't allow for routing the call to a particular Proc based on the supplied input.

@jeremyevans
Copy link

I think one of the desired features is that you want to call some blocks passed but not others, and this appears to only work for the case where you want to call all the blocks.

@JEG2
Copy link

JEG2 commented Oct 17, 2011

This works fine, but I prefer the solutions that let me work by block name, instead of position.

@Keoven
Copy link

Keoven commented Oct 17, 2011

Nice solution but I would have preferred if you added something like a conditional chain blocks based upon the return value of the method:

request.
  success {|arg| 'success'}.
  error {|arg| 'failure'}

So in that example maybe let's say the request:

  • returns true -- success block is executed and error block is skipped
  • returns false -- success block is skipped and error block is executed

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