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.
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, @_
Tom: If it were taking `@_[0,1]` as a scalar parameter, it would be printing out `2`. That's what happens with `sprintf(@_)`. 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.
Shoichi: Yes, that's what I ended up figuring out to work around the issue, but I'm trying to understand why the original code is misbehaving (or why my understanding of the way the code should be behaving is incorrect).
Incorrect.
Replace
sprintf
withscalar
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]
withsprintf @_
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.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 tosprintf
as the first parameter, for whichsprintf
wants a string, so it gets stringified, and thensprintf
uses that as its format string. No other parameters are passed tosprintf
but the format string doesn’t demand any so that works out fine.Ah, thank you Aristotle! You have cleared up some subtleties in the language that I knew were there, but were quite hard to pin down. Thank you for taking the time to clearly explain those subtleties, and give an example of how I could detect them in the future (change sprintf to scalar).
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.