10. Subroutines

Introduction

def hello
    $greeted += 1      # in Ruby, a variable beginning with $ is global (can be any type of course)
    puts "hi there!"
end

# We need to initialize $greeted before it can be used, because "+=" is waiting a Numeric object
$greeted = 0
hello                  # note that appending () is optional to function calls with no parameters

Accessing Subroutine Arguments

# In Ruby, parameters are named anyway
def hypotenuse(side1, side2)
    Math.sqrt(side1**2 + side2**2)    # the sqrt function comes from the Math module
end
diag = hypotenuse(3, 4)

puts hypotenuse(3, 4)

a = [3, 4]
print hypotenuse(*a)                  # the star operator will magically convert an Array into a "tuple"

both = men + women

# In Ruby, all objects are references, so the same problem arises; we then return a new object
nums = [1.4, 3.5, 6.7]
def int_all(n)
    n.collect { |v| v.to_i }
end
ints = int_all(nums)

nums = [1.4, 3.5, 6.7]
def trunc_em(n)
    n.collect! { |v| v.to_i }         # the bang-version of collect modifies the object
end
trunc_em(nums)

# Ruby has two chomp version:
# ``chomp'' chomps the record separator and returns what's expected
# ``chomp!'' does the same but also modifies the parameter object

Making Variables Private to a Function

def somefunc
    variable = something  # variable is local by default
end

name, age = ARGV
start     = fetch_time

a, b = pair               # will succeed if pair is an Array object (like ARGV is)
c = fetch_time

# In ruby, run_check can't access a, b, or c until they are
# explicitely defined global (using leading $), even if they are
# both defined in the same scope

def check_x(x)
    y = "whatever"
    run_check
    if $condition
        puts "got $x"
    end
end

# The following will keep a reference to the array, though the
# results will be slightly different from perl: the last element
# of $global_array will be itself an array
def save_array(ary)
    $global_array << ary
end

# The following gives the same results as in Perl for $global_array,
# though it doesn't illustrate anymore the way to keep a reference
# to an object: $global_array is extended with the elements of ary
def save_array(ary)
    $global_array += ary
end

Creating Persistent Private Variables

# In Ruby, AFAIK a method cannot access "local variables" defined
# upper scope; mostly because everything is an object, so you'll
# do the same by defining an attribute or a static attribute

# In Ruby the BEGIN also exists:
BEGIN { puts "hello from BEGIN" }
puts "hello from main"
BEGIN { puts "hello from 2nd BEGIN" }
# gives:
#   hello from BEGIN
#   hello from 2nd BEGIN
#   hello from main

# In Ruby, it can be written as a static method and a static
# variable
class Counter
    @@counter = 0
    def Counter.next_counter; @@counter += 1; end
end

# There is no need of BEGIN since the variable will get
# initialized when parsing
class Counter
    @@counter = 42
    def Counter.next_counter; @@counter += 1; end
    def Counter.prev_counter; @@counter -= 1; end
end

Determining Current Function Name

# You can either get the whole trace as an array of strings, each
# string telling which file, line and method is calling:
caller

# ...or only the last caller
caller[0]

# We need to extract just the method name of the backtrace:
def whoami;  caller()[0] =~ /in `([^']+)'/ ? $1 : '(anonymous)'; end
def whowasi; caller()[1] =~ /in `([^']+)'/ ? $1 : '(anonymous)'; end

Passing Arrays and Hashes by Reference

# In Ruby, every value is a reference on an object, thus there is
# no such problem
array_diff(array1, array2)

def add_vecpair(a1, a2)
    results = []
    a1.each_index { |i| results << (a1[i] + a2[i]) }
    results
end
a = [1, 2]
b = [5, 8]
c = add_vecpair(a, b)
p c

# Add this to the beginning of the function to check if we were
# given two arrays
a1.type == Array && a2.type == Array or
    raise "usage: add_vecpair array1 array2 (was used with: #{a1.type} #{a2.type})"

Detecting Return Context

# There is no return context in Ruby

Passing by Named Parameter

# Like in Perl, we need to fake with a hash, but it's dirty :-(
def thefunc(param_args)
    args = { 'INCREMENT' => '10s', 'FINISH' => '0', 'START' => 0 }
    args.update(param_args)
    if (args['INCREMENT']  =~ /m$/ )
        # .....
    end
end

thefunc({ 'INCREMENT' => '20s', 'START' => '+5m', 'FINISH' => '+30m' })
thefunc({})

Skipping Selected Return Values

# there is no "undef" direct equivalent but there is the slice equiv:
a, c = func.indexes(0, 2)

Returning More Than One Array or Hash

# Ruby has no such limitation:
def somefunc
    ary = []
    hash = {}
    # ...
    return ary, hash
end
arr, dict = somefunc

array_of_hashes = fn
h1, h2, h3      = fn

Returning Failure

return
# or (equivalent)
return nil

Prototyping Functions

# You can't prototype in Ruby regarding types :-(
# Though, you can force the number of arguments:
def func_with_no_arg; end
def func_with_no_arg(); end
def func_with_one_arg(a1); end
def func_with_two_args(a1, a2); end
def func_with_any_number_of_args(*args); end

Handling Exceptions

raise "some message"        # raise exception

begin
    val = func
rescue Exception => msg
    $stderr.puts "func raised an exception: #{msg}"
end

# In Ruby the rescue statement uses an exception class, every
# exception which is not matched is still continuing
begin
    val = func
rescue FullMoonError
    ...
end

Saving Global Values

# Saving Global Values
# Of course we can just save the value and restore it later:
def print_age
    puts "Age is #{$age}"
end

$age = 18         # global variable
print_age()
if condition
    safeage = $age
    $age = 23
    print_age()
    $age = safeage
end

# We can also use a method that saves the global variable and
# restores it automatically when the block is left:

def local(var)
    eval("save = #{var.id2name}")
    begin
        result = yield
    ensure
        # we want to call this even if we got an exception
        eval("#{var.id2name} = save")
    end
    result
end

condition = true
$age = 18
print_age()
if condition
    local(:$age) {
        $age = 23
        print_age()
    }
end
print_age()

# There is no need to use local() for filehandles or directory
# handles in ruby because filehandles are normal objects.

Redefining a Function

# In Ruby you may redefine a method [but not overload it :-(]
# just by defining again with the same name.
def foo; puts 'foo'; end
def foo; puts 'bar'; end
foo
#=> bar

# You can also take a reference to an existing method before
# redefining a new one, using the `alias' keyword
def foo; puts 'foo'; end
alias foo_orig foo
def foo; puts 'bar'; end
foo_orig
foo
#=> foo
#=> bar

# AFAIK, there is no direct way to create a new method whose name
# comes from a variable, so use "eval"
colors = %w(red blue green yellow orange purple violet)
colors.each { |c|
    eval <<-EOS
    def #{c}(*a)
        "<FONT COLOR='#{c}'>" + a.to_s + "</FONT>"
    end
    EOS
} 

Trapping Undefined Function Calls with AUTOLOAD

def method_missing(name, *args)
    "<FONT COLOR='#{name}'>" + args.join(' ') + "</FONT>"
end
puts chartreuse("stuff")

Nesting Subroutines

def outer(arg)
    x = arg + 35
    inner = proc { x * 19 }
    x + inner.call()
end

Program: Sorting Your Mail

#!/usr/bin/ruby -w
# mailsort - sort mbox by different criteria
require 'English'
require 'Date'

# Objects of class Mail represent a single mail.
class Mail
    attr_accessor :no
    attr_accessor :subject
    attr_accessor :fulltext
    attr_accessor :date

    def initialize
        @fulltext = ""
        @subject = ""
    end

    def append(para)
        @fulltext << para
    end

    # this is called if you call puts(mail)
    def to_s
        @fulltext
    end
end

# represents a list of mails.
class Mailbox < Array

    Subjectpattern = Regexp.new('Subject:\s*(?:Re:\s*)*(.*)\n')
    Datepattern = Regexp.new('Date:\s*(.*)\n')

    # reads mails from open file and stores them
    def read(file)
        $INPUT_RECORD_SEPARATOR = ''  # paragraph reads
        msgno = -1
        file.each { |para|
            if para =~ /^From/
                mail = Mail.new
                mail.no = (msgno += 1)
                md = Subjectpattern.match(para)
                if md
                    mail.subject = md[1]
                end
                md = Datepattern.match(para)
                if md
                    mail.date = DateTime.parse(md[1])
                else
                    mail.date = DateTime.now
                end
                self.push(mail)
            end
            mail.append(para) if mail
        }
    end

    def sort_by_subject_and_no
        self.sort_by { |m|
            [m.subject, m.no]
        }
    end

    # sorts by a list of attributs of mail, given as symbols
    def sort_by_attributs(*attrs)
        # you can sort an Enumerable by an array of
        # values, they would be compared
        # from ary[0] to ary[n]t, say:
        # ['b',1] > ['a',10] > ['a',9]
        self.sort_by { |elem|
            attrs.map { |attr|
                elem.send(attr)
            }
        }
    end

end

mailbox = Mailbox.new
mailbox.read(ARGF)

# print only subjects sorted by subject and number
for m in mailbox.sort_by_subject_and_no
    puts(m.subject)
end

# print complete mails sorted by date, then subject, then number
for m in mailbox.sort_by_attributs(:date, :subject)
    puts(m)
end