Friday, 13 February 2009

The Ruby Proc

In Ruby a Proc (short for procedure) is a block of code, bound to a variable (closely related to a block, discussed here). As is always the way in Ruby, a Proc is an object, and so can be created with the new method, though, as discussed later, it is generally preferable to use the lambda method. Here are some examples (expanding on those in the Ruby documention):
# A Proc can have one, none or many arguments
times7 = Proc.new {|n| n * 7 }
statement = Proc.new { 'from statement' }
multiply3 = Proc.new {|x, y| x * y }

The code in the Proc object can be invoked by using the call method.
p times3.call(12)               #=> 36
p times5.call(5) #=> 25
p times7.call(8) #=> 56
p times3.call(times5.call(4)) #=> 60

p statement.call #=> "from statement"
p multiply.call(4, 3) #=> 12

You can pass around Proc objects like any other object:
def gen_times(factor)
return Proc.new {|n| n*factor }
end

class ProcTest
# Class variable is a Proc
@@times13 = Proc.new {|n| n * 13 }

# Method uses various Proc objects
def test
times11 = Proc.new {|n| n * 11 }
p times11.call(3)
p @@times13.call(7)
times2 = gen_times(2)
p times2.call(19)
end

# Method uses a Proc passed as an argument
def test_argument prc
p prc.call(4)
end
end

# ProcTest object instantiated, and methods called
pt = ProcTest.new
pt.test
pt.test_argument Proc.new {|x| x + 5}

Instead of using call, you can invoke the code using square brackets notation. The following are equivalent:
multiply.call(4, 3)
multiply[4, 3]


Proc.new vs lambda
If you call a Proc with too few arguments, Ruby will pad them out with the nil object, so multiply.call(14) above would invoke the Proc code with 14 and nil (which would generate an error in this case). Any extra arguments are simply discarded.

The Kernal has a method lambda (there is also a method proc, which reportedly does the same as Proc.new, but I found it identical to lambda) which will also give a Proc object, but in this case the Proc will raise an ArgumentError is the argument count is wrong.
count_nils_new = Proc.new {|x, y, z|
"#{x.nil?} #{y.nil?} #{z.nil?}"
}

count_nils_lambda = lambda {|x, y, z|
"#{x.nil?} #{y.nil?} #{z.nil?}"
}

# The 4 is quietly discarded
p count_nils_new.call(1, 2, 3, 4)

# The method is sent 1, 2, nil
p count_nils_new.call(1, 2)

# The method is sent nil, nil, nil
p count_nils_new.call

# This is fine
p count_nils_lambda.call(1, 2, 3)

# These will all generate an ArgumentError
p count_nils_lambda.call(1, 2, 3, 4)
p count_nils_lambda.call(1, 2)
p count_nils_lambda.call

Proc objects have an arity method that can be used to determine how many arguments the Proc is expecting.

Using return in a Proc
Using an explicit return in a Proc object created with Proc.new (but not lambda) will cause the calling method to return that value.
def with_return_and_new
prc = Proc.new { return 'This is printed' }
prc.call
'Never seen'
end

def no_return_with_new
prc = Proc.new { 'no_return_with_new' }
prc.call
'This is printed'
end

def with_return_and_lambda
prc = lambda { return 'with_return_and_lambda' }
prc.call
'This is printed'
end

p with_return_and_new
p no_return_with_new
p with_return_and_lambda

It seems that the Proc object is returning not just the value, but the return command too. In general, therefore, it is a bad idea to use an explicit return inside a Proc object defined with Proc.new (if only because the effect will be confusing to anyone with out a good understanding of Ruby peculiarities). This is discussed more here:
http://innig.net/software/ruby/closures-in-ruby.rb

API for the Proc object:
http://www.ruby-doc.org/core/classes/Proc.html

See also:
http://eli.thegreenplace.net/2006/04/18/understanding-ruby-blocks-procs-and-methods/
http://blog.sidu.in/2007/11/ruby-blocks-gotchas.html

Struggling with Ruby: Contents Page

5 comments:

Unknown said...

Thanks for going over this. I didn't quite 'get it' when reading through the Pick-axe.

Your examples and some time in irb helped.

Jeremy

lewis said...

Thanks, that's great.

jeremyrdavis said...

Very helpful post.

D.V.Abhilash said...

times7 = Proc.new {|n| n * 7 }
p times7.call(12) #=> 36
p times7.call(5) #=> 25
p times7.call(8) #=>56

are the method calls with respect to the Below mentioned post correct..
p times3.call(12) #=> 36
p times5.call(5) #=> 25
p times7.call(8) #=> 56

when i executed the code in the block...the code gave some error.

Supriya Surve said...

Thanks for the post, its really helpful. Got much clear idea about procs.