Learning Ruby: methods vs procs (or Ruby vs Python?)

October 4th 2010 01:03 pm

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!, see the end for John Leach’s lovely response via email at the time)

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!)

John Leach’s thought provoking tuppence

Young padawan, you look but you do not see, you will learn

or rather

Yeah, but blocks are closures Tom

Tom goes to google and comes back with http://www.artima.com/intv/closures2.html
Matz

I think it’s not that useful in the daily lives of programmers. It doesn’t matter that much.

Then john came back with

I can think of one example in Rails right away where it's useful, transactions:

r = Record.new params[:record]

Record.transaction do
 r.save
 RecordLog.create(:text => "created a new record")
end


that code takes some input from a browser (in params), instantiates a
new Record object, then writes it and a RecordLog entry to the database
atomically.
All the Record.transaction does is sends a BEGIN to the db server,
executes the block, and sends a COMMIT (or a ROLLBACK if the block
errors for any reason).
The block needs access to the r object. We could have created that
inside the block, but then it'd need access to the params object. So
without real closure support, Record.transaction would have had to
support passing in arbitrary variables.
Remember, that interview with Matz was in 2003 - more people are using
Ruby for more things nowadays, for uses beyond the imagination of it's
creator I'm sure :)

Final Thoughts
I am waiting to be blown away by Ruby and Rails

Posted by tom under Python & Ruby | No Comments »

Trackback URI | Comments RSS

Leave a Reply