Learning Ruby: methods vs procs (or Ruby vs Python?)
October 4, 2010
I have been meaning to learn ruby for a while and the place I am working now uses a lot so I had another look at it. I read Learn To Program, a simple but good book and found the bit on blocks and procs etc pretty good and wanted to see if I could do the stuff in Python as well. Python has anonymous “lambda” functions but they are limited to one line a subset of the syntax which is a bit annoying sometimes. My worry with methods in Ruby is that they are not first class, I think because you can omit parenthesis and so you have no way of referring to them without invoking them.
I remembered this while reading the SICP book, the question was about the difference between this program in applicative and normal order evaluation
(define (p) (p))
(define (test x y)
(if (= x 0)
0
y))
It rang a bell as (define (p) p)
does not go into an infinite loop if you invoke p. In lisp (p)
calls the procedure p with no arguments whereas p
is just a reference to the function. In python someinstance.method
refers to the method, someinstance.method()
calls it, Ruby seems to need Proc objects to get around this (IMHO as a beginner)
I redid all the examples from the book in Python
Eg 1
Ruby
def maybe_do some_proc
if rand(2) == 0
some_proc.call
end
end
def twice_do some_proc
some_proc.call
some_proc.call
end
wink = Proc.new do
puts '<wink>'
end
glance = Proc.new do
puts '<glance>'
end
Python
import random
def maybe_do(some_proc):
if random.choice(range(2)) == 0:
some_proc()
def twice_do(some_proc):
some_proc()
some_proc()
def wink():
print 'wink'
def glance():
print 'glance'
for i in range(5):
print 'running for i=',i
maybe_do(wink)
Eg2
Ruby
def do_until_false first_input, some_proc
input = first_input
output = first_input
while output
input = output
output = some_proc.call input
end
input
end
build_array_of_squares = Proc.new do |array|
last_number = array.last
if last_number <= 0
false
else
# Take off the last number...
array.pop
# ...and replace it with its square...
array.push last_number*last_number
# ...followed by the next smaller number.
array.push last_number-1
end
end
always_false = Proc.new do |just_ignore_me|
false
end
puts do_until_false([5], build_array_of_squares).inspect
yum = 'lemonade with a hint of orange blossom water'
puts do_until_false(yum, always_false)
Python
def do_untill_false(first_input, some_proc):
input = first_input
output = first_input
while output:
input = output
output = some_proc(input)
return input
def build_array_of_squares(array):
last_number = array.pop()
if last_number <= 0:
return False
else:
array.append(last_number * last_number)
array.append(last_number - 1)
return array
def always_false(just_ignore_me):
return False
def just_ignore_me():
pass
print do_untill_false([5], build_array_of_squares)
yum = 'lemonade with a hint of orange blossom water'
print do_untill_false(yum, always_false)
Eg3
Ruby
def compose proc1, proc2
Proc.new do |x|
proc2.call(proc1.call(x))
end
end
square_it = Proc.new do |x|
x*x
end
double_it = Proc.new do |x|
x+x
end
double_then_square = compose double_it, square_it
square_then_double = compose square_it, double_it
puts double_then_square.call(5) puts square_then_double.call(5)
Python
def compose(proc1,proc2):
def composed(x):
return proc2(proc1(x))
return composed
def square_it(x):
return x**2
def double_it(x):
return x*2
double_then_square = compose(double_it,square_it)
square_then_double = compose(square_it,double_it)
print double_then_square(5)
print square_then_double(5)
Eg4
class Array
def each_even(&was_a_block__now_a_proc)
# We start with "true" because
# arrays start with 0, which is even.
is_even = true
self.each do |object|
if is_even
was_a_block__now_a_proc.call object
end
# Toggle from even to odd, or odd to even.
is_even = !is_even
end
end
end
fruits = ['apple', 'bad apple', 'cherry', 'durian']
fruits.each_even do |fruit|
puts "Yum! I just love #{fruit} pies, don't you?"
end
[1, 2, 3, 4, 5].each_even do |odd_ball|
puts "#{odd_ball} is NOT an even number!"
end
Python
class MyArray(list):
def each_even(self):
for i in range(len(self)):
if i % 2 == 0:
yield self[i]
fruits = MyArray(['apple', 'bad apple', 'cherry', 'durian'])
for fruit in fruits.each_even():
print 'yum! I love %s pies, dont you?' % fruit
for odd_ball in MyArray([1,2,3,4,5]).each_even():
print '%s is NOT an even number' % odd_ball
Eg5
Ruby
def profile block_description, &block
start_time = Time.new
block.call
duration = Time.new - start_time
puts "#{block_description}: #{duration} seconds"
end
profile '25000 doublings' do
number = 1
25000.times do
number = number + number
end
puts "#{number.to_s.length} digits"
# That's the number of digits in this HUGE number.
end
profile 'count to a million' do
number = 0 1000000.times do
number = number + 1
end
end
Python
def profile(description, function):
import time
start_time = time.time()
function()
duration = time.time() - start_time
print '%s: %s seconds' % (description, duration)
print function.__name__
print 'see, "function.__name__" can be used in place of description in python'
def count_to_a_million():
number = 0
for i in range(1000000):
number = number+1
profile('count to a million', count_to_a_million)
def profiled(function):
def new_function(*args, **kwargs):
import time
start_time = time.time()
result = function(*args, **kwargs)
print function.__name__, 'took', time.time() - start_time, 'secs'
return result
return new_function
@profiled
def count_to_a_million_again():
number = 0
for i in range(1000000):
number = number + 1
count_to_a_million_again()
This uses decorators, a nice Python feature that uses higher order functions (and the fact functions are first class in python).
In Conclusion
IMHO, at this point in my experience of Ruby, with all the disclaimers about my non expert status etc. Like:
- No restriction on complexity of anonymous functions
Dont Like:
- Methods being different from Procs/Blocs, non-uniform syntax
- Leaving out parenthesis (though I await DSL goodness later!)
- “end” everywhere (I know the indentation thing in python is contentious!)