Ruby, blocks and procs

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

  « Previous: Next: »