Why can't I concatenate and print the value returned by a function?

I have a Perl program that calls a function, in this case ref, and checks the result. Specifically, I am testing that a variable is a hash reference. In that case, ref will return 'HASH'. I tested it and it worked.

Then I decided to log it, adding a print that displays the result of the same call, but it didn't work correctly. Here is a reduced version:

use strict;
use warnings;

my $book_ref = {};
$book_ref->{'title'} = 'The Lord of the Rings';

if (ref $book_ref eq 'HASH') {
    print "ref \$book_ref is a " . ref $book_ref . "\n";
}

print "Program is over\n";

To my surprise, this was the output:

ref $book_ref is a Program is over

And despite using strict and warnings there were neither errors nor warnings.
The call to ref is exactly the same (it's a copy and paste), but while it works correctly inside the if condition, print doesn't display anything, and actually seems to be interrupted, as the newline character is clearly skipped. Why does the behaviour change?

Upvotes: 1

Views: 164

Answers (1)

The reason is that the function ref is called without parentheses, and this causes the line to be parsed incorrectly.

  • When ref is called inside the if, the condition is clearly delimited by parentheses, which means that ref knows perfectly well what its argument is: $book_ref. There's no ambiguity.
  • Instead, when printing the result, the lack of parentheses means that Perl will parse the line in a way that was not intended:

    1. First it will concatenate $book_ref and "\n". In scalar context, $book_ref evaluates to a string like HASH(0x1cbef70), therefore the result is the string "HASH(0x1cbef70)\n"
    2. Then, ref will be called on the string "HASH(0x1cbef70)\n", producing as output an empty string: ''.
    3. At this point, print prints the empty string, that is, nothing, and stops there. The newline character \n is skipped because it has already been consumed by ref, so print doesn't even see it. And there are no errors.

All of this descends from Perl's operator precedence: from the table,

(...)

 8.    left         + - .
 9.    left         << >>
10.    nonassoc     named unary operators
11.    nonassoc     < > <= >= lt gt le ge
12.    nonassoc     == != <=> eq ne cmp ~~

(...)

where the "function call" is actually a "named unary operator" (the unary operator being ref). So the . operator at line 8 has higher precedence than the function call at line 10, which is why the result of print is not the expected one. On the other hand, the function call has higher precedence than eq (at line 12), which is why inside the if everything works as expected.

The solution to precedence problems is to use .

  • A possibility is to use parentheses to emphasize the function call:

    print "ref \$book_ref is a " . ref($book_ref) . "\n";
    
  • Another one, which I like less but nevertheless works, is to use parentheses to isolate the string that must be concatenated, by putting the opening bracket just before ref:

    print "ref \$book_ref is a " . (ref $book_ref) . "\n";
    
  • Another possible approach, suggested by zdim in a comment, is to use commas:

    print "ref \$book_ref is a ", ref $book_ref, "\n";
    

When I first wrote the if I decided to avoid the parentheses to make the code more readable. Then I copied it and didn't notice the problem. I ended up wasting 2 hours to find the bug.

The best solution seems to be the first one, because if you copy it to another place (like another print) you are guaranteed to also copy the parentheses that prevent the problem. With the second one I probably wouldn't realize how important the parentheses are and wouldn't copy them. And the third one works only if you remember that you have to always use commas and not dots, which is not obvious and therefore error prone. So, although they work, I consider them less safe.

Other comments have also suggested to use printf, which requires dealing with format specifiers, or expression interpolation, like print "ref \$book_ref is a ${\ ref $book_ref }\n";, which I find harder to read.

Bottom line: always use the parentheses.

Upvotes: 7

Related Questions