Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

Surprising result when using undef

by davebaker (Pilgrim)
on Feb 22, 2021 at 20:39 UTC ( #11128664=perlquestion: print w/replies, xml ) Need Help??

davebaker has asked for the wisdom of the Perl Monks concerning the following question:

I was surprised at this result when I tried to write a couple of lines of code that would prevent an email from going out to an active mailing list, unless I (as the developer) had expressly set "is_dev_mode" to be 0 or 1 in an anonymous hash supplied as the parameter. Without expressly telling the subroutine whether or not we're in development mode, I figured, the subroutine needed to assume that we are, so as not to actually send an email to all of my important customers. So:

send_email ( { subject => "Test subject", body => "Test body", } ); sub send_email { my $param = shift; my $subject = $param->{subject}; my $body = $param->{body}; my $is_dev_mode = exists $param->{is_dev_mode} ? $param->{is_dev_mode} : undef; $is_dev_mode = 1 unless ( $is_dev_mode == 0 || $is_dev_mode == 1 ) ; # In case the CGI param is ''; default is to be "safe" (is_de +v_mode == 1) _blast_email_to_all_my_important_customers( $subject, $body ) unles +s ( $is_dev_mode ); }

So I run the code, and... did all my important customers just get an embarrassing test email?

They sure did. Now, let's add "use strict;" and "use warnings;" and try it again, and throw in a warn statement to try to figure out what's happened.

use strict; use warnings; send_email ( { subject => "Test subject", body => "Test body", } ); sub send_email { my $param = shift; my $subject = $param->{subject}; my $body = $param->{body}; my $is_dev_mode = exists $param->{is_dev_mode} ? $param->{is_dev_mode} : undef; $is_dev_mode = 1 unless ( $is_dev_mode == 0 || $is_dev_mode == 1 ) ; # In case the CGI param is ''; default is to be "safe" (is_de +v_mode == 1) warn "is_dev_mode is '", $is_dev_mode, "'\n"; _blast_email_to_all_my_important_customers( $subject, $body ) unles +s ( $is_dev_mode ); }

Result:

Use of uninitialized value $is_dev_mode in numeric eq (==) at C:\Users +\davel\Desktop\test-of-dev-mode-flag-revised.pl line 19. Use of uninitialized value $is_dev_mode in warn at C:\Users\davel\Desk +top\test-of-dev-mode-flag-revised.pl line 23. is_dev_mode is ''

Thanks for the warning, but the customers just got ANOTHER email blast.

OK, I see how $is_dev_mode was undef, and hence the warning. But if $is_dev_mode is undef, then $is_dev_mode is not equal to zero, right? And if $is_dev_mode is undef, then $is_dev_mode is not equal to 1, right? And if neither is true, then the "unless it's true" clause means $is_dev_mode gets 1 as its value (so as to avoid the embarrassing blast to all my important customers), right? Nope! is_dev_mode is still undefined.

An interesting and surprising result to me. I don't think I'll be setting variables to undef and using them in this kind of boolean test again.

I would not have thought that "( $an_undefined_variable == 0 )" would evaluate to true because, golly, undef isn't a number and so the "==" operator is testing whether one number is the same as another number. Now, in hindsight of course, it's apparent that the "==" operator is going to convert both sides to a number in order to chug along and do the best it can, and 0 seems to be the closest thing to a number for undef. I think.

Replies are listed 'Best First'.
Re: Surprising result when using undef
by syphilis (Bishop) on Feb 22, 2021 at 23:11 UTC
    I would not have thought that "( $an_undefined_variable == 0 )" would evaluate to true because, golly, undef isn't a number and so the "==" operator is testing whether one number is the same as another number.

    The "==" operator is comparing the left hand side and the right hand side in numeric context.
    And, in numeric context, undef and 0 are equivalent:
    C:\>perl -e "print 'ok' if undef == 0;" ok C:\>
    But in string context, it's a different matter:
    C:\>perl -e "print 'ok' if undef eq 0;" C:\>

    Cheers,
    Rob
Re: Surprising result when using undef
by swl (Priest) on Feb 22, 2021 at 20:55 UTC

    undef is not the same as zero.

    I see a couple of options you could use.

    use 5.010; my $is_dev_mode = $param->{is_dev_mode} // 1;

    Or for perls older than 5.10:

    my $is_dev_mode = defined $param->{is_dev_mode} ? $param->{is_dev_mode +} : 1;

    And if you want to get a bit more obtuse and do all the value handling on one line then you can use the double negative ("bang bang" operator). That will convert any non-zero (or true) value to a 1, and any false value to perl's false. Then that can be converted to a zero.

    # it would likely help your future self to add a comment here if you +do this my $is_dev_mode = !!$param->{is_dev_mode} || 0;

    Another alternative is to use a boolean flag to test for non-dev mode, instead of dev mode. Then you only need to check for truthiness of the passed flag to determine whether to send the emails or not. That avoids all the value checking and changing, simplifying your code. It also means that if the flag is not passed with a true value then the emails will not be sent.

Re: Surprising result when using undef
by jcb (Parson) on Feb 23, 2021 at 02:39 UTC

    Perl's undefined value becomes the empty string if coerced to a string, and the empty string becomes zero if coerced to a number. Therefore, an undefined value will compare equal to zero in numeric comparison. Perl can emit warnings whenever an undefined value is coerced, as you have found.

    The smallest change to fix your code is to change the undef on line 17 to 1, thus changing the default value if is_dev_mode is not given to a true value instead of an undefined value.

    Alternately, as another monk suggested, change is_dev_mode to is_live and simply load my $is_live = $param->{is_live}; in the prologue. If the is_live key is not given, that will set $is_live to undef, and undef is false in boolean context, without a warning.

Re: Surprising result when using undef
by LanX (Cardinal) on Feb 22, 2021 at 20:50 UTC
    So your problem is ...

    DB<1> p undef == 0 1 DB<2>

    correct???

    Perl does automatic type-casting² and emits warnings at best.

    That's why you can do things like

    my $count; $count++; print "Count is $count"

    (undef->integer->string)

    If you want to test against undef use operators like defined or //

    edit

    > I would not have thought that "( $an_undefined_variable == 0 )" would evaluate to true because, golly, undef isn't a number and so the "==" operator is testing whether one number is the same as another number.

    Perl is not C, types are dynamic. That's why Perl has so many more operators than other languages like + and . where others only have + °

    BTW: JS has the worst of both worlds in this respect, types are dynamic but operators are spare. I.e. sometimes 1 + 1 means 11 there.

    And in Python 3/2 results in 1 , because you divided two integers! (IIRC they adjusted something in Py3 but I think it's a new operator // )

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

    update

    °) Perl has also two equality operators for numeric == and string eq

    That's why

    DB<1> p undef eq "" 1 DB<2> p undef eq "0" DB<3>

    ²) i.e. internally. Externally do all scalars have a well defined numeric and string value.

Re: Surprising result when using undef
by haukex (Bishop) on Feb 23, 2021 at 17:28 UTC

    In addition to the others' reponses, see Truth and Falsehood, which discusses the topic in detail.

Re: Surprising result when using undef
by davebaker (Pilgrim) on Feb 23, 2021 at 19:33 UTC

    I think I got too distracted by the way I was using undef inside if statement -- which is evaluating a variable or expression in boolean context -- and the fact that the expression is a test for numeric equality --

     if ( $possibly_undefined_val_here == 0 )

    ... because I've routinely used and understood something like  if ( $possibly_undefined_val_here ) to evaluate to "falsey" when the variable is undefined. I hadn't thought about it evaluating to 0, such that the resulting 0 == 0 in the boolean test becomes true, but of course it is.

    But I also see that basically I got balled up in seeing that what I wanted was to have a flag that said "OK, we're in production mode because we have a truthy value, so let's roll" rather than one that said "OK, let's not roll if we're in development mode, so check to see whether we have a truthy value as to that, and, in order to be sure we remembered the issue, let's check to see whether we explicitly set the development mode flag to be the digit one or the digit 0."

    I'm afraid this kind of complicated, inverted thinking gets in my way a lot. How elegant and direct is the logic of testing for the truth of a production mode flag, like testing to be sure a link in a chain is in place.

    Really appreciate this information!

      I think that you have a good understanding!

      I'm not sure that it is 100% clear, but the // operator can be used like +=,*=, etc. I don't use any language other than Perl that has this operator. But this is a cool operator (added in Perl 5.10).

      In my coding, the most common occurrence would be something like $token //= ''; after some regex that might fail (and hence yield an undef). This means: if $token is defined leave it alone. However if it is undefined, set it to the null string. I do that right after the regex. This prevents warnings in comparisons and other ugliness with the DBI later on.

      In your code, perhaps:

      $possibly_undefined_val_here //= 1; if ( $possibly_undefined_val_here == 0 ){} #now always defined
      However as I think you see now, it is much better not to intentionally generate any undef value on your own. The equivalent thing to //= in Perl <5.10 requires an if statement. Numerous studies show that an "if" statement is at least 10x more likely to contain an error - no matter how simple appearing the "if" statement might look. Your woes are a case in point.

      Update:
      Since your code is obviously intended to have different behavior in the production mode vs the debug mode. One feature of Perl that perhaps you should be aware of is the constant pragma.

      It is possible to write:

      use constant DEBUG => 1; if (DEBUG) {}
      If you change to: use constant DEBUG => 0; The if statements with DEBUG won't even be compiled! So there is no run-time penalty for code like that in the production version. By normal convention, constants are all CAPS although there is no language enforcement of that convention.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://11128664]
Approved by LanX
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others chanting in the Monastery: (7)
As of 2021-04-15 12:27 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?