What's going on here?

What do you think the following lines print?

use feature 'say';
sub fmt { sprintf( @_[0, 1] ) }
my $num = 1_234_567_890.12_345_678_9;
say sprintf( '%.6f', $num );
say fmt( '%.6f', $num );

I think the two say lines should both print out the same value, 1234567890.123457. The first line behaves as expected, but the second does not. Does anybody have any idea on why?

Playing around with the code, I’ve figured out how to fix the issue, but I’m curious as to thoughts and explanations from the community.

8 Comments

Try this:
sub fmt { sprintf $_[0], $_[1] }

indeed:



$ perl -E'say prototype(\&CORE::sprintf)'
$@

so the sprintf() call will take the first parameter - @_[0,1] - in scalar context, and there are no futher parameters.

Might as well be explicit, and do something like: my $fmt = shift; sprintf $fmt, @_

If it were taking `@_[0,1]` as a scalar parameter, it would be printing out `2`.

Incorrect.

Replace sprintf with scalar and you will see that the result is exactly the same as you see here.

That’s because @_[0,1] is a list expression, not an array, and list expressions in scalar context yield the last value from that list, for symmetry with the comma operator in scalar context (which does not create a list!).

Next, replace sprintf @_[0,1] with sprintf @_ and you will see that that result is exactly what you said it should be.

That’s because @_ is an array expression, and those do yield their length in scalar context.

It's clearly taking in two parameters, but for some reason the second parameter is being coerced from a number to a string, formatted in the default manner (with only 15 digits (10 digits before the decimal and 5 after)), and then coerced back to a number which is then formatted and printed.

The only part of this that’s correct is that the second parameter is being stringified with the default stringification. Every other part of this theory is wrong.

What happens is that @_[0,1] is evaluated to a list, then scalar context is applied, yielding the last value from that list, then this value is passed to sprintf as the first parameter, for which sprintf wants a string, so it gets stringified, and then sprintf uses that as its format string. No other parameters are passed to sprintf but the format string doesn’t demand any so that works out fine.

Just for the record: Perl6 catches the error.


sub fmt { sprintf @_[0,1] }
# Your printf-style directives specify 1 argument, but no argument was supplied

sub fmt { sprintf $^a, $^b } # OK!

While you are correct about how it works, conceptually the issue is simply that an array slice in scalar context produces the last element of the slice.

That it actually produces a list and then the last element of the list is produced is an implementation detail.

This is a fine example of why prototypes should not be used on user-created subroutines; just memorizing what they are for the built-ins and expecting the syntactic effects they have is hard enough.

Leave a comment

About $Bob

user-pic I blog about Perl.