John M. Dlugosz has asked for the wisdom of the Perl Monks concerning the following question:

I have a value that can be specified as a single number or as a range, and I use e.g. [3,5] for the range and a plain scalar if only one value is needed. So, you can call foo(5) or foo([5,6])

So, I wrote

if (ref $value eq "ARRAY") {
to test my parameter. But, I got to thinking. What if it's not really an array, but a class that looks like an array, or a tie? Should I use isa instead?

If I eventually turn the range into a class by blessing the array reference and adding some members to, say, get the difference, then the isa would be handy. I think a tie would still test as an array.

But, in a related thought, I have some code that wants a number. Now, what if it's not really a number, but a 'bignum' class that overloads all the operators so it (mostly) can be used like a number? In this case, ideally I wouldn't care, because if it quacks like a duck it must be a duck. I want to test for the quack, not the duck. For $x+2 I don't care that $x is a class object not a scalar, but what about as an argument to pack? I suspect that would not work properly.

So, how much testing should one do to see if arguments are what are expected, and how does "overloading" functions to understand different data types complicate matters?

Any thoughts?

—John

Replies are listed 'Best First'.
Re: if (ref $value eq "ARRAY") {
by Aristotle (Chancellor) on Aug 23, 2002 at 03:27 UTC

    When I first started writing modules, I took a paranoid approach. The problem was, I had to get more and more paranoid as I discovered more and more variations of possible erroneous data input. My sanity checks turned more and more into insane checks.

    After a while, I took a step back. This couldn't be it.

    I thought about it and came to the conclusion that most of the time, if I'm given a totally wrong type of input (scalar when I expected a hashref, or something similar), it is a rare case that the program can recover gracefully. Most of the time it results in aborted execution one way or another. It may be a bit nicer if you give the caller a chance to clean up before he croaks, but essentially you can rarely deal with these errors in any other way than having the programmer fix the code, plain and simple.

    Considering this, I decided to stick to semantic checks and leave the syntactical ones out. If I expect an arrayref, I may croak "not an arrayref" unless ref $param but won't go out of my way to ensure that it's really an arrayref. If I get a nonref and try to dereference it, Perl will die screaming. That's ok, we know something went wrong. I will however check whether scalar @$param reports the right number of elements, because that won't cause Perl to die screaming, it will simply make my code malfunction, leaving the user-programmer with no obvious clue about what went wrong.

    The benefit is similar to all the other instances where Perl merely asks you to play by the rules rather than wielding a shotgun to force you to: I don't need to spend millenia coding sanity checks for every conceivable constellation under the sun, and in turn someone using my module can do something I didn't think of without having to spend millenia trying to trick my "sanity" checks.

    Perl is laid back. Perl programmers should be laid back too. False paranoia is a waste of energy.

    Makeshifts last the longest.

        I thought about it and came to the conclusion that most of the time, if I'm given a totally wrong type of input (scalar when I expected a hashref, or something similar), it is a rare case that the program can recover gracefully.

      Even if you could recover gracefully from wrong input, it's probably a bad idea. If you get wrong input, then somewhere along the line someone screwed up, and about the worst thing you can do is hide the problem (or at least pass it on to another chunk of code further away from the original) and make it harder to debug. That sort of false "defensive programming" isn't just a waste of time, it's actively harmful.

      In general, I like to write code that fails -- gracefully, mind you, not locking up the machine or any such -- as soon as possible on bad assumptions. Makes it easier to find bugs.

      --
      F o x t r o t U n i f o r m
      Found a typo in this node? /msg me
      The hell with paco, vote for Erudil!

        That's what I was getting at. It's not actually possible to really recover from such errors in a sensible way, so I want to croak at some point anyway. Since Can't use string ("...") as an ARRAY ref is just as good as a custom croak message, I simplify my life by skipping checks for conditions that wil cause the code to blow up anyway. Instead I check for semantic problems where I need to hit the selfdestruct button myself because Perl won't know to do it for me.

        Makeshifts last the longest.

Re: if (ref $value eq "ARRAY") {
by blakem (Monsignor) on Aug 23, 2002 at 02:42 UTC
    Not an answer to how much testing, but on what tests are available...

    You might want to check out the reftype() and blessed() subs from Scalar::Util (core module as of 5.8) They're designed to cleanly split the kludgy dual nature of ref() into two separate tests.

    -Blake

Re: if (ref $value eq "ARRAY") {
by dws (Chancellor) on Aug 23, 2002 at 02:50 UTC
    But, I got to thinking. What if it's not really an array, but a class that looks like an array, or a tie? Should I use isa instead?

    One of the downsides of a weekly-typed language is that you have to jump through lots of hoops if you want to protect yourself from every concievable ill that clients of your APIs can heap upon you.

    So, how much testing should one do to see if arguments are what are expected, and how does "overloading" functions to understand different data types complicate matters?

    Do as much testing as you need to, though if you find yourself using isa a lot, that's an indication that your interfaces need further thinking. A technique from Smalltalk, which I've seen used a bit in Java, is "double dispatch". It's a technique that, in part, lets you contain a geometric explosion in type testing when you introduce new types of objects. Look it up and study it. Double dispatch can be done in Perl.

      I think some weakly-typed languages do better. To reflect back on the big thread from the other day that all languages evolve toward Lisp, Common Lisp allows for "overloading" functions based on type or arbitrary tests. So, one could separate things out into broad different things, but still be flexible within each thing.

      I guess that's just syntactic sugar around the test I could write inside the function, now.

      I think the concept here is that "all differences in allowed parameter types should be done through polymorphism". In Perl, that's hard to do with anything that's not blessed.

      But, it does suggest something. I could have an abstract base class to serve as an "interface", and recognise that case as well as the built-in things (plain scalar and array ref). Then, anyone wanting to pass an object and have it mean something to this function would simply need to make it "isa" that interface class.

      — John

Re: if (ref $value eq "ARRAY") {
by tstock (Curate) on Aug 23, 2002 at 02:43 UTC
    try:
    if (UNIVERSAL::isa($value, 'ARRAY') {
    Tiago
    update: sorry about the hasty response... nothing to see here, move along now :)
Re: if (ref $value eq "ARRAY") {
by particle (Vicar) on Aug 23, 2002 at 13:49 UTC
    here's a little code to test a straight-up array ref, an object, and a tied array...

    also, has it been blessed? might be of some interest to you.

    #!/usr/bin/perl use strict; use warnings; $|++; package tiedarray; use Tie::Array; our @ISA=('Tie::StdArray'); package main; for my $t_ref ( [], bless([]), tie(@{[]}, 'tiedarray') ) { if( ref $t_ref ) { print q{i'm a ref}, $/; if( UNIVERSAL::can( $t_ref, 'isa' ) ) { print q{i'm an object, too}, $/; } } print $/; } __END__ # prints: i'm a ref i'm a ref i'm an object, too i'm a ref i'm an object, too
    i don't think it's too much to test two cases if( ref $t_ref and ! UNIVERSAL::can($t_ref, 'isa') ), given the benefits.

    ~Particle *accelerates*

      Hmm, so a tied ref is blessed automatically, also?
Re: if (ref $value eq "ARRAY") {
by defyance (Curate) on Aug 23, 2002 at 02:54 UTC
    This wont answer your question, its just some personal wisdom(not that I have much of that). This is one of those "If I were in a simmilar situation" type things.

    Say I was think of something that could be done to possible make the outcome of a situation better, since I've gone as far to think it up, I may as well go ahead and do it!

    Unless, of course, your on a time constraint...

    Nothing more to see here, move along

    --~~--~~--~~--~~--~~--~~--~~--~~--~~--~~--~~--~~--~~--
    perl -e '$a="3567"; $b=hex($a); printf("%2X\n",$a);'

Re: if (ref $value eq "ARRAY") {
by fglock (Vicar) on Aug 23, 2002 at 12:11 UTC

    I think this code will hear most quacks.

    use strict; sub store_value { my @range; push @range, (ref $_[0] eq "ARRAY") ? @{$_[0]} : @_; print "range is ",join(",",@range),"\n"; } store_value(1); store_value(1..3); store_value([1..3]);

    If you add  while (shift) { ... } it will work even with (1, 2, [3, 4], 5)