9

I am trying to figure the best way to differeniate in Perl between cases where an argument has not been passed, and where an argument has been passed as 0, since they mean different things to me.

(Normally I like the ambiguity, but in this case I'm generating SQL so I want to replace undefined args with NULL, but leave 0 as 0.)

So this is the ambiguity:

sub mysub {
  my $arg1 = shift;
  if ($arg1){
    print "arg1 could have been 0 or it could have not been passed.";
  }
}

And so far, this is my best solution... but I think it is a little ugly. I'm wondering if you can think of a cleaner way or if this looks OK to you:

sub mysub {
  my $arg1 = (defined shift) || "NULL";
  if ($arg1 ne "NULL"){
    print "arg1 came in as a defined value.";
  }
  else {
    print "arg1 came in as an undefined value (or we were passed the string 'NULL')";
  }
}
darch
  • 4,200
  • 1
  • 20
  • 23
Stephen
  • 8,508
  • 12
  • 56
  • 96

5 Answers5

17

Here's an example of how you can handle all the possible cases:

sub mysub {
    my ($arg1) = @_;
    if (@_ < 1) {
        print "arg1 wasn't passed at all.\n";
    } elsif (!defined $arg1) {
        print "arg1 was passed as undef.\n";
    } elsif (!$arg1) {
        print "arg1 was passed as a defined but false value (empty string or 0)\n";
    } else {
        print "arg1 is a defined, non-false value: $arg1\n";
    }
}

(@_ is the array of arguments to your function. Comparing it to 1 here is counting the number of elements in the array. I'm intentionally avoiding shift, as it alters @_, which would require us to store the original size of @_ somewhere.)

  • Thanks, this is nice and thorough. I hadn't thought about the difference between receiving an undef value and not receiving a value (not relevant in my case, but good to think about). – Stephen Nov 28 '11 at 17:32
3

What about:

sub mysub {
    my ( $arg ) = @_;

    if ( @_ == 0 ) {
        print "arg did not come in at all\n";
    } elsif ( defined $arg ) {
        print "arg came in as a defined value.\n";
    } else {
        print "arg came in as an undefined value\n";
    }
}

mysub ();
mysub ( undef );
mysub ( 1 );

Update: I added the check if there was anything passed in at all. But that can only be useful if you are expecting a single parameter. If you would like to get multiple params and need to differentiate between undefined and omitted parameters, take a hash.

sub mysub_with_multiple_params {
    my %args_hash = @_;

    for my $expected_arg ( qw( one two ) ) {
        if ( exists $args_hash{ $expected_arg } ) {
            if ( defined $args_hash{ $expected_arg } ) {
                print "arg '$expected_arg' came in as '$args_hash{ $expected_arg }'\n";
            } else {
                print "arg '$expected_arg' came in as undefined value\n";
            }
        } else {
            print "arg '$expected_arg' did not come in at all\n";
        }
    }
}

mysub_with_multiple_params ();
mysub_with_multiple_params ( 'one' => undef, 'two' => undef );
mysub_with_multiple_params ( 'one' => 1, 'two' => 2 );

And btw: If you have to do any steps to sanitize your params, don't do it yourself. Have a look at cpan and especially Params::Validate

Egga Hartung
  • 1,061
  • 11
  • 23
  • `elsif (@_ == 0) { print "arg1 did not come in at all"; } ...` ? – mob Nov 28 '11 at 18:38
  • @mob: I wasn't sure if thats the only parameter coming in. `$arg1` sounds like there were more. But I will add it and recommend passing a hash if there are more parameters to be checked... – Egga Hartung Nov 29 '11 at 09:34
1

The only way to be sure is to inspect the length of @_ to see if there was an argument in that slot. This can be seen as a little complicated when there are also mandatory arguments, but it doesn't have to be. Here's a pattern used in a lot of object accessors:

package Foo;

sub undef_or_unset {
    my ($self, @arg) = @_;

    return 'unset' unless @arg;
    my ($val) = @arg;

    return 'undef' unless defined $val;
    return 'defined';
}

package main;
use Test::More tests => 3;

my $foo = bless {} => 'Foo';

is($foo->undef_or_unset(), 'unset');
is($foo->undef_or_unset(undef), 'undef');
is($foo->undef_or_unset('bluh'), 'defined');
darch
  • 4,200
  • 1
  • 20
  • 23
1

personally I like keeping undef to represent NULLs - it matches what DBI placeholders/DBIx::Class/SQL::Abstract all do, and the risk with setting it to be the string "NULL" is that you will accidentally insert the string, rather than NULL itself.

If you're using a recent version of Perl (5.10 or above), then check out the 'defined-or' operators // and //= which are particularly handy for processing arguments.

regarding the SQL, if you want to generate the SQL string, you could end up with something like this:

sub mysub {
  my ($args) = @_;
  my @fields = qw/ field1 field2 field3 /;
  my $sql = "INSERT INTO mytable (field1,field2,field3) VALUES (" .
   join(',', map { ("'".$args->{$_}."'") // 'NULL' ) } )
    .")";
  return $sql;
}

edit (to answer the bit about NULL and undef):

Using DBI handles with placeholders:

my $sth = $dbh->prepare('INSERT INTO mytable (field1,field2,field3) '.
                        'VALUES (?,?,?)');

# undef will set a NULL value for field3 here:
$sth->execute( "VAL1", "VAL2", undef );

DBIx::Class

DBIx::Class - same principle - pass in an undef value to create a NULL in the database:

my $rs = My::Schema->resultset('MyTable');
my $obj = $rs->create({
   field1 => 'VAL1',
   field2 => 'VAL2',
   field3 => undef,    # will set a `NULL` value for field3 here
});
plusplus
  • 1,992
  • 15
  • 22
  • I have a question about your statement that using undef to represent NULLs "matches what DBI placeholders/DBIx::Class/SQL::Abstract all do" -- can you elaborate on that point? I'm not totally sure what you mean. – Stephen Nov 28 '11 at 18:44
0

map is your friend. Try this:

function("joe",undef); # should print "joe" and "NULL"
function("max",38);    # should print "max" and "38"
function("sue",0);     # should print "sue" and "0"   

sub function {
    my($person,$age) = map { $_ // "NULL" } @_;
    print "person: $person\n";
    print "age:    $age\n";
}

To add a little more color, I've become a fan of using a hash as the arguments for code clarity and to remove the importance of remembering arugment ordering. So rewritten it would look like this:

function2(person=>"joe",age=>undef); # should print "joe" and "NULL"
function2(person=>"max",age=>38);    # should print "max" and "38"

sub function2 {
    my(%args) = map { $_ // "NULL" } @_;
    print "person: $args{person}\n";
    print "age:    $args{age}\n";
}

(Updated: to handle 0 correctly and then again to use the // operator.)

Patrick Collins
  • 4,046
  • 3
  • 26
  • 29
  • Your code fails to differentiate between zero and undef (http://ideone.com/MD6gN), so it really just demonstrates the same problem the question already did. – Rob Kennedy Nov 28 '11 at 17:44