Ruby response to perl.com article on overloading

djberg96 on 2003-07-23T22:08:21

This is a journal entry about Ruby and it's kinda long, so move along now if you aren't interested.

Specifically, this is a response to Dave Cross' article "Overloading" on perl.com, a problem I have with Dave's code and how Ruby is just better at OO. You've been warned twice now. Hopefully I won't get kicked off of use.perl or anything...

My first *major* issue is with the fact that Dave returns undef in the event of a failure when creating a new Fraction object (except in one case, where it croaks). Why not croak on the spot for all failures? Dave's approach could cause great confusion, forcing me to track down "can't call method on 'undef'" error messages later on. I won't be able to tell right away if the constructor failed or if the Fraction object was undef'd somewhere later on accidentally. Why not at least give me the chance to wrap the constructor in an eval so I can trap that error if I'm so inclinded? As it is I'll probably be stepping into the debugger at some point, or I'll be forced to write extra tests to account for this possibility. Blech.

Ok, on to a Ruby version of the code. Let's start with the base class:

# Note that the '@' can be replaced with 'self' if you prefer # normalise() method skipped since it wasn't actually provided in the article class Fraction attr_reader :num, :den def initialize(num=nil,den=nil) if num && den unless num.kind_of?(Fixnum) && den.kind_of?(Fixnum) raise TypeError, "two-argument form accepts Fixnum's only" end @num = num @den = den elsif num && den.nil? unless num.kind_of?(Fraction) raise TypeError, "one-argument form accepts Fraction type only" end @num = num.num @den = num.den else @num = 0 @den = 1 end end end First, this just *looks* cleaner, mkay? Specifically, I'm using actual parameter names in the constructor instead of $_[0] and $_[1], plus the ->{x}, "ref", and UNIVERSAL::isa syntax is an eyesore IMO. In addition, it's slightly less code.

Second, while I can't declare a type for the parameters (hey, this ain't Java, ok? - but see below), I can give them a name and a default value. This provides an extra advantage over the Perl equivalent in that I can never pass more than two arguments without causing an error.

Third, because everything in Ruby is an object, and all objects have a type, we can use the kind_of? method to test the actual type of each argument rather than resorting to regular expressions, which is clunky. It works in Dave's code, but in general I think that approach is prone to error.

On to the overloading. Dave provides an "add" method, then overloads the "+" operator. Here's the Ruby equivalent: def +(n) if n.kind_of?(Fraction) num = (@num * n.den) + (n.num * @den) den = @den * n.den return Fraction.new(num,den) elsif n.kind_of?(Fixnum) # Single digit return self + Fraction.new(n,1) elsif n =~ /(\d+)\/(\d+)/ return self + Fraction.new($1.to_i,$2.to_i) else raise ArgumentError, "Can't add a " + n.class.to_s + " to a Fraction" end end alias add + No special keywords required and about half the code. Just define the "+" method as you see fit. This also has the added advantage of causing an error if you try to pass more than one argument, while Dave's version silently ignores extra arguments contained within @_. To be fair, though, almost no one checks the argument length to methods in Perl (including me) because it's so rarely an issue.

While Ruby doesn't include any built-in mechanism for overloading, there *is* a package on the RAA called "overload" (go figure) written by Nobu Nakada that allows you to achieve the same effect. Consider:

# Java weenies take note require "overload" class Fraction def initialize # ... end overload(:initialize)

def initialize(x) # ... end overload(:initialize,Fraction)

def initialize(x,y) # ... end overload(:initialize,Fixnum,Fixnum) end

# Now I can do this ...

a = Fraction.new b = Fraction.new(a) c = Fraction.new(1,2)

# ... and Ruby will call the appropriate constructor
The best of all worlds.

There. My Ruby proselytizing is done for the day. Don't kill me.


Hmmm

pdcawley on 2003-07-24T08:40:07

Surely:


class Fraction

    def +(n)
        return n.add_fraction(self)
    end

    def add_fraction(n)
        return Fraction.new((@num * n.den) + (n.num * @den),
                                                                @den * n.den)
    end

    def add_string(str)
        str =~ /(\d+)\/(\d+)/ or
            raise ArgumentError "Can't add '" + str + "' to " +
                self.to_str + ": Badly formed string"
        return Fraction.new($1.to_i, $2.to_i)
    end

    def add_fixnum(n)
        return self + Fraction.new(n.to_i, 1)
    end
end

class Fixnum
    def add_fraction(n)
        n.add_fixnum(self)
    end
end

class String
    def add_fraction(n)
        n.add_string(self)
    end
end

class Object
    def add_fraction(n)
        raise ArgumentError, "Can't add a " + self.class.to_s + " to a Fraction"
    end
end

is rather better Ruby.style

Re:Hmmm

djberg96 on 2003-07-24T15:31:25

Hmm..probably. Perhaps I was being too faithful to Dave's code (someone else on IRC already said something similar). In Ruby, as in Perl, TMTOWTDI.

Returning "undef" vs croaking

davorg on 2003-07-24T10:27:53

My first *major* issue is with the fact that Dave returns undef in the event of a failure when creating a new Fraction object (except in one case, where it croaks).

Actually, returning "undef" from the constructor is a vital part of the way the module works when it comes to constant overloading. It's the one instance of using "croak" that is a bug.