30 Aug 2011
Some notes on ruby, blocks, and procs.
Manually creating blocks
Ruby has three ways of manually creating blocks: Proc.new, lambda, and proc. They have slightly different behaviour, and the behaviour also varies between Ruby 1.8 and 1.9!
- lambda checks that the number of arguments passed matches the number of block parameters
- whereas Proc.new doesn’t check (however the block may raise an error, depending on it’s code)**
- and proc behaves like lambda in Ruby 1.8, and like Proc.new in Ruby 1.9. So, avoid using proc!
A bit of code to demonstrate this:
multiplier_l = lambda { |a, b| puts "a * b is: #{a*b}" } multiplier_p = Proc.new { |a, b| puts "a * b is: #{a*b}" } multiplier_l.call( 3,4,5 ) ArgumentError: wrong number of arguments (3 for 2) multiplier_p.call( 3,4,5 ) a * b is: 12 > multiplier_p.call( 1 ) TypeError: nil can't be coerced into Fixnum # in this case, Proc handled one param, but block errored
And now using rvm to switch between Ruby versions:
RUBY_VERSION => "1.8.7" multiplier_p = proc { |a, b| puts "a * b is: #{a*b}" } multiplier_p.call( 3,4,5 ) ArgumentError: wrong number of arguments (3 for 2) RUBY_VERSION => "1.9.2" multiplier_p = proc { |a, b| puts "a * b is: #{a*b}" } multiplier_p.call( 3,4,5 ) a * b is: 12
Scoping
In Ruby 1.8, block parameters can overwrite parameters of the same name in the current scope; in Ruby 1.9 they’re protected.
> hello = "hello" > def frenchy > x = "bonjour" > yield x > end > puts hello hello > frenchy { |hello| puts hello } bonjour # as expected > puts hello bonjour # ouch! In 1.9 you'd get "hello"
&block
Some of the Rails and Ruby library code define methods with &block as the last parameter to capture an anonymous block.
- anonymous blocks are ignored if they’re not used, and &block is an optional parameter that must appear as the last parameter
- it’s effectively a type-checked parameter – it will only accept an anonymous block or a proc (if proceeded with &)
- the block can be called with call or yield
- you can check if a block was passed using block_given?
- &block is sort of an “invisible parameter” at the end of all methods. But by explicitly using &block, callers get more flexibility when using your method ie they can pass in a proc (perhaps defined elsewhere and used multiple times)
Anonymous blocks are ignored if they’re not used:
> def foo(a) > puts "a is #{a}" > end > foo(1) a is 1 > foo(1) { puts "2" } a is 1
Conversely if &block is declared as a parameter, using it is optional:
> def foo(a, &block) > puts "a is #{a}" > end > foo(1) a is 1
Procs can be called with call; anonymous blocks can be called or yielded to.
> hello = lambda { puts "good bye" } # define a proc for later use > def foo(a, b, &block) > puts "a is #{a}" > b.call # proc with call > block.call # block with call > yield # block with yield > end > foo(1, hello) { puts "fred" } # with an anonymous block a is 1 good bye fred fred > foo(1, hello, &hello) # with a proc; notice & syntax a is 1 good bye good bye good bye
You can check if an anonymous block was supplied using block_given?
> def foo(a, &block) > puts "a is #{a}" > block.call if block_given? > yield if block_given? > end > foo(1) { puts "mary" } a is 1 mary mary > def foo(a) # or, without defining the block parameter > puts "a is #{a}" > yield if block_given? # therefore can only yield not call > end
&block is sort of an “invisible parameter” at the end of all methods. But by explicitly using &block, callers get more flexibility when using your method:
> def foo(a) # no &block defined in parameters > puts "a is #{a}" > yield if block_given? > end > foo(1) { puts "john" } # works as expected a is 1 john > foo(1, hello) ArgumentError: wrong number of arguments (2 for 1) # dang! I can't use my super-duper hello proc
Precedence
do .. end has weaker precedence than { }. For example, if foo and bar are both methods:
These are both the same ie the method foo receives two parameters, bar and a block.
foo bar do |s| puts(s) end foo(bar) do |s| puts(s) end
And these are both the same ie foo and bar both receive one parameter; foo the call to bar, and bar a block:
foo bar { |s| puts(s) } foo( bar { |s| puts(s) } )
Of course the moral of story is not to rely on obscure precedence rules, rather use parentheses whenever something is unclear – as always, in any language.
Closures
Blocks are closures ie they store or carry the value local variables from the the original scope into a different scope. They’re another way of reusing the same logic with slightly different values. For example:
> def build_header( level ) > return lambda { |text| "<#{level}>#{text}</#{level}>" } > end > h1 = build_header("h1") > h1.call("Examples") <h1>Examples</h1> > h2 = build_header("h2") > h2.call("Details") <h2>Details</h2>comments powered by Disqus