extending ruby - handling errors

J

Jason Lillywhite

I want to raise an ArgumentError, "Function only takes numeric objects."
if a non-numeric argument is passed to the existing Ruby method,
Numeric.ceil.

#Here is my feeble attempt to extend Ruby:

irb(main):001:0> class Numeric
irb(main):002:1> alias :eek:ld_ceil :ceil
irb(main):003:1> def ceil
irb(main):004:2> raise ArgumentError,
irb(main):005:2* "function only takes numeric objects." unless
self.is_a? Numeric
irb(main):006:2> self.old_ceil
irb(main):007:2> end
irb(main):008:1> end
=> nil

#But that does not work - I can't get past the call on the object

irb(main):009:0> "foo".ceil
NoMethodError: undefined method `ceil' for "foo":String
from (irb):9

Could someone point me in the right direction? Thank you.
 
J

Jesús Gabriel y Galán

I want to raise an ArgumentError, "Function only takes numeric objects."
if a non-numeric argument is passed to the existing Ruby method,
Numeric.ceil.

#Here is my feeble attempt to extend Ruby:

irb(main):001:0> class Numeric
irb(main):002:1> =A0 alias :eek:ld_ceil :ceil
irb(main):003:1> =A0 def ceil
irb(main):004:2> =A0 =A0 raise ArgumentError,
irb(main):005:2* =A0 =A0 =A0 "function only takes numeric objects." unles= s
self.is_a? Numeric
irb(main):006:2> =A0 =A0 self.old_ceil
irb(main):007:2> =A0 end
irb(main):008:1> end
=3D> nil

#But that does not work - I can't get past the call on the object

irb(main):009:0> "foo".ceil
NoMethodError: undefined method `ceil' for "foo":String
=A0 =A0 =A0 =A0from (irb):9

Could someone point me in the right direction? Thank you.

I'm not sure if what you are trying to achieve makes sense or not. The
reason is that you don't want to check an argument to a method, you
are trying to check that the *receiver* of the method call is of a
certain type. And that cannot be done inside the Numeric.ceil or in my
opinion it doesn't even make sense. Why? Because how do you know that
String or any other object doesn't have a valid ceil method? In fact,
when you call ceil on a string it's a completely different and
unrelated method to ceil in any other class. For example, say I do
this:

class String
def ceil
"this is the ceil of #{self}"
end
end

Then, I should be able to call "foo".ceil and you shouldn't be telling
that that is wrong :).

On the other hand, if you want to check whether the method ceil can be
called on a specific object, that can be checked using respond_to?:

irb(main):001:0> "foo".respond_to? :ceil
=3D> false
irb(main):002:0> 3.respond_to? :ceil
=3D> true

But I'm not sure what you are trying to achieve, so I don't know if
this is useful for you. On the other hand, if for some strange reason
you want to convert NoMethodError into ArgumentError, you could try
something like this (although I don't see any utility in this):

irb(main):039:0> class Object
irb(main):040:1> def method_missing meth, *args, &blk
irb(main):041:2> raise ArgumentError, "#{meth} called on #{self.class}"
irb(main):042:2> end
irb(main):043:1> end
=3D> nil
irb(main):044:0> "foo".ceil
ArgumentError: ceil called on String
from (irb):41:in `method_missing'
from (irb):44
from =03:0

I mean, calling a non-existing method is not an ArgumentError, it's a
NoMethodError :)


Hope this helps,

Jesus.
 
B

Ben Giddings

I want to raise an ArgumentError, "Function only takes numeric objects."
if a non-numeric argument is passed to the existing Ruby method,
Numeric.ceil. ...
irb(main):009:0> "foo".ceil
NoMethodError: undefined method `ceil' for "foo":String
from (irb):9

Right, because "foo" is a String, and Strings aren't numerics. If you want
all non-numerics to recognize that "ceil" is a valid call, but only for
Numerics, you'd need to provide "ceil" to all objects, then override it for
numerics:

irb(main):001:0> class Object
irb(main):002:1> def ceil
irb(main):003:2> raise ArgumentError, "not a numeric!"
irb(main):004:2> end
irb(main):005:1> end
=> nil
irb(main):006:0> "Foo".ceil
ArgumentError: not a numeric
...
irb(main):005:0> (3.14).ceil
=> 4

This is probably the wrong approach though. You shouldn't load up "Object" so
it knows about random other methods that it's subclasses define, you should
just catch NoMethodError in an appropriate spot. For example:

def calculate_bounding_box(width, height)
begin
width_bound = width.ceil
height_bound = height.ceil
rescue NoMethodError
raise ArgumentError, "width and height must be numeric"
end
width_bound * height_bound
end


Ben
 
J

Jason Lillywhite

Ben said:
def calculate_bounding_box(width, height)
begin
width_bound = width.ceil
height_bound = height.ceil
rescue NoMethodError
raise ArgumentError, "width and height must be numeric"
end
width_bound * height_bound
end

Thank you. I see that what I wanted is not a good idea. Doing a catch is
better.
 
G

Gary Wright

This is probably the wrong approach though. You shouldn't load up
"Object" so
it knows about random other methods that it's subclasses define, you
should
just catch NoMethodError in an appropriate spot. For example:

def calculate_bounding_box(width, height)
begin
width_bound = width.ceil
height_bound = height.ceil
rescue NoMethodError
raise ArgumentError, "width and height must be numeric"
end
width_bound * height_bound
end

I would argue that this is also the wrong approach because it
confuses responsibilities. To use programming by contract terminology,
the requirement that the actual arguments to calculate_bounding_box
be numeric (or more specifically that they respond to ceil) is a
pre-condition and pre-conditions should be guaranteed by the caller,
not the callee. If pre-conditions aren't met, then all bets
are off about how the callee will behave. Water under the bridge so
to speak.

Also, the example as written doesn't really solve the problem since
the caller will have to deal with ArgumentError instead of
NoMethodError. In either case it indicates that the *caller* is
shirking its duty to guarantee the pre-conditions. Adding code in
the caller to guarantee numeric arguments is probably clearer than
arranging to respond to NoMethodError or ArgumentError.

The correct solution is to fix the buggy code that is *calling*
calculate_bounding_box to guarantee the pre-condition.

Gary Wright
 
J

Jason Lillywhite

Gary said:
and pre-conditions should be guaranteed by the caller,
not the callee. If pre-conditions aren't met, then all bets
are off about how the callee will behave. Water under the bridge so
to speak.

Could you give me a simple example of guaranteeing pre-conditions by the
caller? I'm having some trouble visualizing this. Thank you.
 
G

Gary Wright

Could you give me a simple example of guaranteeing pre-conditions by
the
caller? I'm having some trouble visualizing this. Thank you.

The simplest case is when the arguments are already known
to be numeric in which case you don't have to do anything:

calculate_bounding_box(3.4, 5.6)

This would also be the case if you are calling from a method
with the same precondition (i.e. numeric arguments). The caller
is once again responsible for the arguments, not the intermediate
method:

def bigger_box(a,b)
calculate_bounding_box(2*a,2*b)
end

If you are getting the arguments from some untrustworthy source:

width, height = get_dodgy_dimensions()

if [width, height].all? { |x| Numeric === x }
box = calculate_bounding_box(width, height)
else
# report error, raise exception, etc.
end

The caller is in the best position to know what to
do if the arguments aren't numeric (e.g. don't call the method!).

If you get rid of the explicit test in my previous example:

width, height = get_dodgy_dimensions()
box = calculate_bounding_box(width, height)

It is clear from the name 'get_dodgy_dimensions' that this is
a really bad idea. That code is broken because it is passing
along unsanitized data. The solution isn't to change
calculate_bounding_box to check its arguments but instead is
fix the *source* of the problem

width, height = get_dodgy_dimensions()
width, height = sanitize_dimensions(width, height)
box = calculate_bounding_box(width, height)

Gary Wright
 
J

Jason Lillywhite

Gary said:
width, height = get_dodgy_dimensions()
width, height = sanitize_dimensions(width, height)
box = calculate_bounding_box(width, height)

That makes sense. Thank you!
 
B

Ben Giddings

I would argue that this is also the wrong approach because it
confuses responsibilities. To use programming by contract
terminology,
the requirement that the actual arguments to calculate_bounding_box
be numeric (or more specifically that they respond to ceil) is a
pre-condition and pre-conditions should be guaranteed by the caller,
not the callee.

If you're writing calculate_bounding_box() as a bit of library code
that will be used by other programmers, it is nice to provide helpful
error messages.

Someone using the method calculate_bounding_box() has no way of
knowing that you're calling "ceil" internally, so getting an error
back saying "NoMethodError: undefined method `ceil' for "2343":String"
will be confusing.

If you are nice enough to catch that error and throw a more useful
error: "ArgumentError: width and height must be numeric" it will make
debugging much easier.

This doesn't mean it's not the responsibility of the caller to make
sure it provides proper arguments, it just means that in the event the
caller doesn't, you get clearer error messages.
Also, the example as written doesn't really solve the problem since
the caller will have to deal with ArgumentError instead of
NoMethodError.

That depends on what you mean by "solve the problem". If the problem
is "my program isn't working", solving the problem will involve
looking at the traceback and figuring out what went wrong. In which
case "ArgumentError: width and height must be numeric" is more helpful
than "NoMethodError: undefined method `ceil'..."
In either case it indicates that the *caller* is
shirking its duty to guarantee the pre-conditions. Adding code in
the caller to guarantee numeric arguments is probably clearer than
arranging to respond to NoMethodError or ArgumentError.

There's no reason not to do both. Clearly, whatever calls
calculate_bounding_box() should make sure the parameters it supplies
are what the method expects, but there's no reason
calculate_bounding_box() can't anticipate a common exception, catch it
then raise a more accurate and more helpful exception of its own.

Ben
 
J

Jesús Gabriel y Galán

On Aug 20, 2009, at 14:49, Gary Wright wrote:
If you're writing calculate_bounding_box() as a bit of library code that
will be used by other programmers, it is nice to provide helpful error
messages.

Someone using the method calculate_bounding_box() has no way of knowing that
you're calling "ceil" internally, so getting an error back saying
"NoMethodError: undefined method `ceil' for "2343":String" will be
confusing.

He will know when he sees that error, if it wasn't in the
documentation of the method.
If you are nice enough to catch that error and throw a more useful error:
"ArgumentError: width and height must be numeric" it will make debugging
much easier.

The duck typing philosophy says that the argument need not be Numeric,
only respond to "ceil" :)
So if you change ArgumentError to say: "#{x} should respond to :ceil",
isn't that pretty similar to the NoMethodError?

Jesus.
 
B

Ben Giddings

He will know when he sees that error, if it wasn't in the
documentation of the method.

Right, and will sit there confused saying "What? What does 'ceil' =20
have to do with anything?" as opposed to "Oh, that method expects =20
numeric arguments."
The duck typing philosophy says that the argument need not be Numeric,
only respond to "ceil" :)
So if you change ArgumentError to say: "#{x} should respond to :ceil",
isn't that pretty similar to the NoMethodError?

Duck typing is why you catch the exception from 'ceil' rather than do =20=

this:

# don't do this
if (not weight.class =3D=3D Numeric) or (not height.class =3D=3D =
Numeric)
raise ArgumentError, "weight and height must be numeric"
end

If someone passes in an argument that "quacks like a Numeric" and =20
responds to ceil, they won't get an error message about invalid =20
arguments because no exception will be thrown.

Saying "should respond to `ceil'" is misleading anyhow. You don't =20
just want the arguments to respond to the method call, you want them =20
to do it in a way that's equivalent to how a Numeric would respond -- =20=

i.e. returns an integer greater-than or equal to the argument.

You could make the error message: ArgumentError, =20
"calculate_bounding_box() calls `ceil' on `width' and `height' to =20
create two bounding integers, which are then multiplied together. =20
Ensure that any arguments you pass in are either Numeric, or respond =20
to `ceil' in the same way a numeric would.", but for a 7-line method, =20=

that's really overkill.

Ben
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,968
Messages
2,570,153
Members
46,699
Latest member
AnneRosen

Latest Threads

Top