The duck's backside

T

Tobias Weber

Hi,
as a newcomer I miss method overloading with named parameters and types
(Objective-C!). Mainly because my code is riddled with ugly "case
argument.class" and "raise unless Integer". But maybe that's the problem.

What is the Duck way to check arguments? I have to because I'm passing
on to a rather fragile web service.

I only need numbers (ids) and strings in my instance variables, but
considered making a class for each one to catch illegal data in
initialize.
 
R

Robert Klemme

2008/5/28 Tobias Weber said:
as a newcomer I miss method overloading with named parameters and types
(Objective-C!). Mainly because my code is riddled with ugly "case
argument.class" and "raise unless Integer". But maybe that's the problem.

You cannot expect to carry your Objective-C style of programming over
to Ruby. Instead, if you want to have overloading you need to either
use different method names (appropriate if the overloaded methods act
differently) or you can do it with a single method (e.g. if the
overloading was just there to adjust type - for example on version
accepts an int and another accepts a float and internally you continue
processing with a float).
What is the Duck way to check arguments? I have to because I'm passing
on to a rather fragile web service.

The duck way is to not check arguments.
I only need numbers (ids) and strings in my instance variables, but
considered making a class for each one to catch illegal data in
initialize.

It's difficult to answer on such a general scale. Usually, if there
are particular requirements you have on arguments passed to methods
you should check inside the method that receives them. Note, that for
integers there is to_int so you can do:

class Foo
def doit length
@l = length.to_int
end
end

Can you give a more concrete example?

Kind regards

robert
 
T

Tobias Weber

Robert Klemme said:
differently) or you can do it with a single method (e.g. if the
overloading was just there to adjust type - for example on version

And if the types are rather different ducks, would you "case type"?
are particular requirements you have on arguments passed to methods
you should check inside the method that receives them. Note, that for

Yes, but it's uuugly ;)
def doit length
@l = length.to_int
end

Nice, I had forgotten about that one, and to_i won't do as even nil
answers to it.
Can you give a more concrete example?

def doit(a, b, c = nil)
begin
if c.nil? and b > 0
c = "ignore"
elsif b.nil? and c > 0
uaid = "ignore"
else
raise
end
rescue RuntimeError, NoMethodError
raise ArgumentError, "need exactly one number"
end
@http.post("first=#{a}&second=#{b}&third=#{c}")
end
 
E

Eleanor McHugh

def doit(a, b, c = nil)
begin
if c.nil? and b > 0
c = "ignore"
elsif b.nil? and c > 0
uaid = "ignore"
else
raise
end
rescue RuntimeError, NoMethodError
raise ArgumentError, "need exactly one number"
end
@http.post("first=#{a}&second=#{b}&third=#{c}")
end


This is off the top of my head so the usual caveats apply:

def doit a, b, c = nil
begin
case
when b && c
raise
when b && b.respond_to?:)to_int)
uaid = "ignore" if b.to_int > 0
when c && c.respond_to?:)to_int)
c = "ignore" if c.to_int > 0
else
raise
end
rescue
raise ArgumentError, "need exactly one number"
end
@post("first=#{a}&second=#{b.to_int}&third=#{c.to_int}")
end

This makes the exclusion of b and c more explicit, which will make it
much easier for a maintainer to understand what's going on. I've also
made integer conversion explicit and focused the filtering logic on
making sure it's appropriate as that's what you're really trying to
achieve, however this kind of belt-and-braces approach suggests that
you're tackling the problem from the wrong angle and should look at
the data-flow elsewhere in your code.

One possibility would be to rejig the whole thing as follows:

def doit a, v, param = :second
raise ArgumentError, "needs a numeric value" unless v.respond_to?
:)to_int)
v = "ignore" if (param == :third) && v > 0
@post("first=#{a}&second=#{v if param == :second}&third=#{v if param
== :third}")
end

and then evolve it from there, although it's also pretty damn ugly.

Where this is called you could write something along the lines of:

if b && c then
raise ArgumentError, "needs exactly one additional argument"
else
doit a, (b || c), (b.nil? ? :third : :second)
end

which could be further encapsulated as:

doit a, b, c = nil
raise ArgumentError, "needs exactly one additional argument" if b && c
v = b || c
raise ArgumentError, "need a numeric parameter" unless v.respond_to?
:)to_int)
param = b.nil? ? :third : :second
v = "ignore" if (param == :third) && v > 0
@post("first=#{a}&second=#{v if param == :second}&third=#{v if param
== :third}")
end


Ellie

Eleanor McHugh
Games With Brains
http://slides.games-with-brains.net
 
R

Robert Dober

The first idea would be to do something like the following

module MyChecker
MyConstraintError = Class::new RuntimeError

def make_ivar name, value, *allowedClasses
raise MyConstraintError, "...." unless
allowedClasses.any?{ |klass| klass === value }
instance_variable_set name, value
end
end

class MyClass
include MyChecker
def initialize int_val, str_val, val
make_ivar :mad:int, int_val, Integer
make_ivar :mad:str, str_val, String
make_ivar :mad:var, val, Integer, String
....

HTH
Robert
 
T

Tobias Weber

Ups, shoulda been c =
@post("first=#{a}&second=#{b.to_int}&third=#{c.to_int}")

Won't work as the web service actually expects either a number or the
string "ignored"
This makes the exclusion of b and c more explicit, which will make it

First I actually tried exclusive or, but that operates bit-wise on
Integer so the results were strange.

I've always been very comfortable with perl's boolean logic, so I have
to relearn some tricks.

(in Ruby 0 and "" are true)
achieve, however this kind of belt-and-braces approach suggests that
you're tackling the problem from the wrong angle and should look at
the data-flow elsewhere in your code.

So that doit() is only passed legal values? That should be the case, but
I want to make extra sure not to give that foreign web service something
it can't swallow.
def doit a, v, param = :second
raise ArgumentError, "needs a numeric value" unless v.respond_to?
:)to_int)
v = "ignore" if (param == :third) && v > 0

That is essentially overloading doit() for first or second parameter,
and a good idea! So far I just wrapped the http calls in convenience
methods one by one.
 
S

shortcutter

Ups, shoulda been c =


Won't work as the web service actually expects either a number or the
string "ignored"

You can do

@post("first=#{a}&second=#{b.to_int rescue 'ignored'}&third=#{c.to_int
rescue 'ignored'}")

Of course, defining a method for this would be better. How about:

def intify(x)
x.to_int rescue "ignored"
end

def doit(a, b, c = nil)
(b, c = intify(b), intify(c)).select {|x| Numeric === x}.size != 1
and
raise ArgumentError, "need exactly one number"
@http.post("first=#{a}&second=#{b}&third=#{c}")
end

Cheers

robert
 
E

Eleanor McHugh

Ups, shoulda been c =

Which makes things simpler ;)

doit a, b, c = nil
raise ArgumentError, "needs exactly one additional argument" if b && c
v = b || c
raise ArgumentError, "need a numeric parameter" unless v.respond_to?
:)to_int)
@post "first=#{a}&" + (b.nil? ? "second=ignored&third=#{v}" :
"second=#{v}&third=ignored")
end
So that doit() is only passed legal values? That should be the case,
but
I want to make extra sure not to give that foreign web service
something
it can't swallow.


That is essentially overloading doit() for first or second parameter,
and a good idea! So far I just wrapped the http calls in convenience
methods one by one.

I'm not suggesting it's a good idea...


Ellie

Eleanor McHugh
Games With Brains
http://slides.games-with-brains.net
 
M

Mark Wilden

doit a, b, c = nil
raise ArgumentError, "needs exactly one additional argument" if b
&& c
v = b || c
raise ArgumentError, "need a numeric parameter" unless
v.respond_to?:)to_int)
@post "first=#{a}&" + (b.nil? ? "second=ignored&third=#{v}" :
"second=#{v}&third=ignored")
end

This looks like a great solution!

But if you want to check for numeric parameters, would it be better
just to use Numeric === v ? I know this isn't the ducky way, but it
certainly seems more "intentional."

///ark
 
J

Jim Menard

Tobias,
as a newcomer I miss method overloading with named parameters and types
(Objective-C!). Mainly because my code is riddled with ugly "case
argument.class" and "raise unless Integer". But maybe that's the problem.

What is the Duck way to check arguments? I have to because I'm passing
on to a rather fragile web service.

Don't bother. Use your arguments, assuming they are the correct type.
If they are not, then a runtime exception will be thrown. Unless the
input to this function is coming from an untrusted source, the inputs
will always be the correct type. If they are not, then something is
seriously wrong.

Jim
 
E

Eleanor McHugh

This looks like a great solution!

But if you want to check for numeric parameters, would it be better
just to use Numeric === v ? I know this isn't the ducky way, but it
certainly seems more "intentional."

That'd be marginally more readable but to my mind it's overly
cautious: what's the point of using Ruby if you're still going to
follow static typing conventions?

Ellie

Eleanor McHugh
Games With Brains
http://slides.games-with-brains.net
 
M

Mark Wilden

That'd be marginally more readable but to my mind it's overly
cautious: what's the point of using Ruby if you're still going to
follow static typing conventions?

The only point of using Ruby (or any other tool in the universe) is to
get a job done. :) I would never use a Ruby feature just because Ruby
makes it possible.

Numeric === v is not a static typing convention (see http://en.wikipedia.org/wiki/Type_system)
It's just OOP. It says in computer-understandable terms exactly what
you expressed in English: "v is numeric."

I don't even know what the ability to respond to :to_int means. Does
that mean the entity is numeric? Does a numeric string qualify? Does a
floating point number qualify? Could Array support this method? Does
every class that responds to to_int do semantically equivalent things?
(think Cowboy#draw and Artist#draw). The only way to tell is to test
it or look it up. This doesn't make it "marginally" less readable than
Numeric === v, IMO.

Different strokes, of course...

///ark
 
D

David Masover

On May 28, 2008, at 11:45 AM, Eleanor McHugh wrote:

The only point of using Ruby (or any other tool in the universe) is to
get a job done. :) I would never use a Ruby feature just because Ruby
makes it possible.

Put it this way: What's the point of using, say, Erlang, if you never use its
concurrency features?

Granted, Ruby has a bit more going for it, but a lot of the joy of using Ruby
is how much less work you end up having to do when you don't have to think
about types too much.
. It's just OOP. It says in computer-understandable terms exactly what
you expressed in English: "v is numeric."

No, it says "v is of class Numeric." It's very explicit, and makes the
assumption that anything which acts like a number will eventually inherit
from Numeric.
I don't even know what the ability to respond to :to_int means.

I think it would be :to_i, actually... And I'd probably just call foo.to_i,
rather than testing for the existence of to_i.

Another possibility is: v.integer?
Does
that mean the entity is numeric?

In a sense.
Does a numeric string qualify?

Yes, it'll be parsed.
Does a
floating point number qualify?

Yes, it'll be rounded down.
Could Array support this method?

Anything could. Open classes mean you could add one to Array. But out of the
box, it doesn't appear to.
Does
every class that responds to to_int do semantically equivalent things?

Probably -- to_i is very much baked into the language, along with to_s and
friends.

Remember the above -- I could actually completely redefine Array, or Numeric,
etc. So even your assumption that "Numeric === foo" tests for Numeric is
really only based on convention -- you're assuming that no one, anywhere in
your code, is doing stuff like this:

Numeric = nil
(think Cowboy#draw and Artist#draw).

Yes, that is a downside of duck typing, as currently implemented, but doesn't
really apply to :to_i.

Also, context matters. If Cowboy and Artist are both in a GUI widget library,
that Cowboy is asking for trouble. And it's worth asking how the Cowboy came
to be in an art studio, or how the Artist came to be in a saloon -- there are
likely going to be more elegant solutions than

if Cowboy === bill
...
 
J

John Carter

Don't bother. Use your arguments, assuming they are the correct type.
If they are not, then a runtime exception will be thrown.

Yes and No.

If the bug is on the backtrace, this works fine. An exception is
thrown, the backtrace is printed, and you inspect each one and you
find the wrong code, and fix it.

End of story.

If however you are doing, as is quite often done, constructing an
object, passing in some of the instance variables as parameters to the
constructor...

The use that triggers the exception, is quite likely _not_ to have the
buggy line on the backtrace. The buggy line was on the call graph
heading towards the constructor, not the use of that instance.

The duck's backside is not watertight, the duck sinks.

Hence my static_type_check set of utilities...

So in...

class MyObject

def initialize( _a, _b, _c)
@a = _a.quacks_like( :qwaak, :waddle, :swim)
@b = _b.static_type_check String
@c = _c.polymorphic_type_check MyBaseClass
end

# Without the checks above, the exceptions would be thrown here, with the buggyline nowhere on the backtrace!
def my_use
@a.qwaak( @b) + @c
end
end

# Abstract base class for all the type check exceptions
class TypeCheckException < Exception
end

# This exception is thrown in event of a method being invoked with an
# object of the wrong duck type.
class DuckTypingException < TypeCheckException
end

# This exception is thrown in event of a method being invoked with a
# parameter of of the wrong static type.
class StaticTypeException < TypeCheckException
end

# This exception is thrown in event of a method being invoked with a
# parameter of of the wrong polymorphic type.
class PolymorphicTypeException < TypeCheckException
end

class Object

# Raise a DuckTypingException unless the object responds to all symbol.
def quacks_like( *symbols)
symbols.each do |symbol|
raise DuckTypingException, "Duck typing error, expected this object to respond to :#{symbol}, but found class #{self.class}\n\t\t#{symbol.inspect}" unless
respond_to? symbol
end
end

def static_type_check_boolean
raise StaticTypeException, "Static type check error, expected object to be a boolean, found '#{self.class}'
\t\t#{self.inspect}" unless
(self.class == TrueClass) ||
(self.class == FalseClass) ||
(self.class == NilClass)
self
end


def static_type_check( klass)
raise StaticTypeException, "Static type check error, expected object to be exactly class '#{klass}', found '#{self.class}'\n\t\t#{self.inspect}" unless
self.class == klass
self
end

def static_type_check_each( klass)
each do |object|
object.static_type_check klass
end
self
end

def polymorphic_type_check_each( klass)
each do |object|
object.polymorphic_type_check klass
end
self
end

def polymorphic_type_check( klass)
# @@static_caller[caller[1]]+=1
raise PolymorphicTypeException, "Polymorphic type check error, expected object to be a kind of '#{klass}', found '#{self.class}'\n\t\t#{self.inspect}" unless
self.kind_of? klass
self
end

end



John Carter Phone : (64)(3) 358 6639
Tait Electronics Fax : (64)(3) 359 4632
PO Box 1645 Christchurch Email : (e-mail address removed)
New Zealand
 
D

David Masover

class MyObject

def initialize( _a, _b, _c)
@a = _a.quacks_like( :qwaak, :waddle, :swim)
@b = _b.static_type_check String
@c = _c.polymorphic_type_check MyBaseClass
end

# Without the checks above, the exceptions would be thrown here, with the
buggyline nowhere on the backtrace!
def my_use
@a.qwaak( @b) + @c
end
end

I'm fine with that, because 99% of the time, I'll never hit that exception.
When I do, it's usually possible to deal with it -- in this case, you would
find where @a is set, and add a check there.

I think you're arguing to add that check first. I would call that premature
strictness -- but if you have to do it, I like your way.

Oh, also: Most of the time, I use setters/getters (attr_accessor), even when
something's mostly going to be internal. That simplifies this process -- I
know there was a problem with a, so I can override a's setter with something
that performs the same check. But that's also probably overkill -- if a class
can't fit on a page, it's probably too big and should be broken down.
 
D

David Masover

# Raise a DuckTypingException unless the object responds to all symbol.
def quacks_like( *symbols)
symbols.each do |symbol|
raise DuckTypingException, "Duck typing error, expected this
object to respond to :#{symbol}, but found class
#{self.class}\n\t\t#{symbol.inspect}" unless
respond_to? symbol
end
end

Just occurred to me that this does have one flaw: It cannot check for what
happens when method_missing is hit. I doubt that's much of a problem in
practice, though.
 
M

Mark Wilden

Put it this way: What's the point of using, say, Erlang, if you
never use its
concurrency features?

All I can do is repeat what I said: the ONLY point to ANYthing (in
programming) is as a means to accomplish some purpose. If using Erlang
accomplishes a given purpose best, then it should be used, no matter
what its concurrency capabilities. One reason might simply be that a
given programmer is more comfortable in that language than another,
and can therefore accomplish the task faster.
Granted, Ruby has a bit more going for it, but a lot of the joy of
using Ruby
is how much less work you end up having to do when you don't have to
think
about types too much.

I think you mean "classes," not "types." At any rate, I don't see how
Numeric === v involves more work than the alternative, so I don't
think that criterion applies here.
No, it says "v is of class Numeric." It's very explicit, and makes the
assumption that anything which acts like a number will eventually
inherit
from Numeric.

Well, the desired goal is in fact to recognize objects that are
numeric. One of the purposes of classes in OOP is to categorize things.
I think it would be :to_i, actually... And I'd probably just call
foo.to_i,
rather than testing for the existence of to_i.

:to_i is actually rather different, as it will convert a string (even
a non-numeric string like "2a"). That's an example of having to know
the implementation of the method to determine whether testing
responds_to? makes sense. For all I know, :to_i may in fact be the
desired method, but Elizabeth used :to_int, and I assume it was for a
reason.
Remember the above -- I could actually completely redefine Array, or
Numeric,
etc. So even your assumption that "Numeric === foo" tests for
Numeric is
really only based on convention -- you're assuming that no one,
anywhere in
your code, is doing stuff like this:

Numeric = nil

That's not merely being unconventional--that's insanity. :) Anyway, it
applies even more so to methods, which aren't in the global namespace.
Yes, that is a downside of duck typing, as currently implemented,
but doesn't
really apply to :to_i.

Also, context matters. If Cowboy and Artist are both in a GUI widget
library,
that Cowboy is asking for trouble.

That's true, but it wasn't my point. The question is whether all
methods with the same name in all active classes should be
semantically equivalent. I think that's a rather larger assumption
than that Numeric means "numeric."

///ark
 
J

John Carter

I'm fine with that, because 99% of the time, I'll never hit that exception.
When I do, it's usually possible to deal with it -- in this case, you would
find where @a is set, and add a check there.

I think you're arguing to add that check first. I would call that premature
strictness -- but if you have to do it, I like your way.

Depends on what I'm doing. If I'm just doing bog standard coding, I'll
probably skip the checks.

If I'm doing something wildly experimental, or ripping up and
restructuring a large chunk of code, I'll probably splash checks
around with a heavy hand.

Gives me a quick heads up that I'm being stupid. Forces me to think a
little, "what do I really want coming it here? I Dunno really, but it
must quack like this. "

Run the unit test, ooh looky, I'm getting something that doesn't quack
like that. No, no surprise now I think about it, but it holds, has, or
makes a thing that quacks like that. A quick fix in the editor and off
I go again.




John Carter Phone : (64)(3) 358 6639
Tait Electronics Fax : (64)(3) 359 4632
PO Box 1645 Christchurch Email : (e-mail address removed)
New Zealand
 
D

David Masover

All I can do is repeat what I said: the ONLY point to ANYthing (in
programming) is as a means to accomplish some purpose. If using Erlang
accomplishes a given purpose best, then it should be used, no matter
what its concurrency capabilities. One reason might simply be that a
given programmer is more comfortable in that language than another,
and can therefore accomplish the task faster.

Even if the problem doesn't require concurrency, the main reason for choosing
Erlang in the first place is its threading model. If you don't like Erlang's
threading, chances are, the rest of it is done better in other languages.

I'm not saying that we don't want you if you won't do Ruby duck typing. Just
saying that I consider duck typing to be a major draw to Ruby in the first
place.

I kind of feel like you're doing the equivalent of this:

i = 0
while(i < some_array.length)
do_something_with(some_array)
i += 1
end

Yes, Ruby can do that, but I think most of us agree that a major appeal of
Ruby is being able to do this instead:

some_array.each { |item|
do_something_with item
}
Well, the desired goal is in fact to recognize objects that are
numeric. One of the purposes of classes in OOP is to categorize things.

Given single inheritance, you're not going to force everything into the exact
category it belongs. I know I implemented a class to represent DNS serial
numbers and their associated math. It didn't inherit from Numeric, but it did
have to_i.

Maybe that was bad design on my part, but the point of duck typing is that we
don't need to care if it calls itself "Numeric". Instead, we care that it
acts like a Numeric -- it responds_to +, -, and probably to_i and integer?

You've probably heard all this before, of course.
That's not merely being unconventional--that's insanity. :) Anyway, it
applies even more so to methods, which aren't in the global namespace.

Alright, without altering the global namespace, and with very possibly a good
excuse, I could do something like this:

class IpAddress < Integer
def to_s
# return '12.34.56.78'
end
end

Now, that actually won't work without a bit of massaging -- the numeric
classes don't have constructors. And there's already a built in IP address
class, so this would be pointless.

But I'm not sure there's any more reason to believe that something which is a
Numeric (or claims to be) is going to give you the semantics you want, than
to believe the same of something which supports to_i (or to_int, which is
probably more correct).
That's true, but it wasn't my point. The question is whether all
methods with the same name in all active classes should be
semantically equivalent. I think that's a rather larger assumption
than that Numeric means "numeric."

The assumption you're making with Numeric isn't that Numeric means "numeric",
but that all possibly numeric values are contained in Numeric.

I think your use case had something to do with a fragile web service, so this
actually could make a lot of sense to you -- it might even be worth checking
if it's an Integer.

But in the general case, I like that much of my code is actually abstract
enough to swap something of an entirely different type (or class) in and have
it do something useful. Recently, I actually wrote an entire class for
storing things (a two-dimensional map) without knowing what kind of object
I'd be filling it with. I ended up filling it with Symbols.
 
M

Mark Wilden

I think your use case had something to do with a fragile web
service, so this
actually could make a lot of sense to you -- it might even be worth
checking
if it's an Integer.

Wasn't my use case. :)

///ark
 

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

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,581
Members
45,056
Latest member
GlycogenSupporthealth

Latest Threads

Top