Perl / perl5

🐪 The Perl programming language
https://dev.perl.org/perl5/
Other
1.85k stars 524 forks source link

`builtin::stringify` does not document that its argument is in scalar context #22118

Open rwp0 opened 4 weeks ago

rwp0 commented 4 weeks ago

The documentation doesn't mention the effect of builtin::stringify function on arrays.

I expected it to act in a similar fashion to "" double quotes.


perl \
  -E 'my @array = ("one", "two"); say "@array"'

one two # <-- Delimited by space

perl \
  -M experimental=builtin \
  -E 'my @array = ("one", "two"); say stringify @array'

2 # <-- Acts as if in a non-string context (the same result if I replace the "stringify" with "scalar" function here)

Is it that the 2 numerical value is turned internally to a string value here?

Is this behavior normal and the results expected?

Do we need to mention the function's effect on arrays (and perhaps hashes too) in the documentation?

I was assuming that stringify is mostly equal to "" string context (with variable interpolation).

Thanks

mauke commented 4 weeks ago

I'd expect stringify X to be equivalent to "" . X, i.e. evaluate in scalar context and convert to string. Your example behaves the way I expect.

Variable interpolation is more complicated. For scalars it just stringifies, but "@foo" is equivalent to join($", @foo), and "%foo" is just '%foo' (hashes don't interpolate at all). It doesn't make sense to me to use this as our model.

leonerd commented 3 weeks ago

Indeed; builtin::stringify has a prototype of ($), i.e. takes a single scalar argument. It gets parsed as builtin::stringify scalar @array; and thus the expected result is a string containing the decimal representation of the count of items in the array.

leonerd commented 3 weeks ago

However I think it's a valid observation that the documentation doesn't point out this, so maybe some more words could be added to better explain it.

nicomen commented 3 weeks ago

Indeed; builtin::stringify has a prototype of ($), i.e. takes a single scalar argument. It gets parsed as builtin::stringify scalar @array; and thus the expected result is a string containing the decimal representation of the count of items in the array.

Should it be called scalarify instead? (?! Or scalarify_and_then_stringify?) It is confusing if it works like "" but not always?

Edit: Ok, that question isn't that good. But perhaps the subset of stringifying it is doing deserves an even more accurate name?

Edit 2: concatify?

Edit 3:

$ cat test_stringify.t

no warnings 'experimental::builtin';
use builtin qw/stringify/;

use Test::More;

my $str       = "hi",
my $num       = 1.01,
my @array     = ( 1, 2, 3 );
my %hash      = ( a => 8 );
my $array_ref = [ 4, 5, 6 ];
my $hash_ref  = { b => 9 };
my $obj       = bless {}, 'something';

subtest "concatify" => sub {
  is stringify($str),       "" . $str,       'string';
  is stringify($num),       "" . $num,       'number';
  is stringify(@array),     "" . @array,     'array';
  is stringify(%hash),      "" . %hash,      'hash';
  is stringify($array_ref), "" . $array_ref, 'array_ref';
  is stringify($hash_ref),  "" . $hash_ref,  'hash_ref';
  is stringify($obj),       "" . $obj,       'object';
};

subtest "stringify" => sub {
  is stringify($str),       "$str",       'string';
  is stringify($num),       "$num",       'number';
  is stringify(@array),     "@array",     'array';
  is stringify(%hash),      "%hash",      'hash';
  is stringify($array_ref), "$array_ref", 'array_ref';
  is stringify($hash_ref),  "$hash_ref",  'hash_ref';
  is stringify($obj),       "$obj",       'object';
};

done_testing;

$ prove -e './perl -I./lib' -lvr test_stringify.t

test_stringify.t .. 
# Subtest: concatify
    ok 1 - string
    ok 2 - number
    ok 3 - array
    ok 4 - hash
    ok 5 - array_ref
    ok 6 - hash_ref
    ok 7 - object
    1..7
ok 1 - concatify
# Subtest: stringify
    ok 1 - string
    ok 2 - number
    not ok 3 - array

    #   Failed test 'array'
    #   at test_stringify.t line 27.
    #          got: '3'
    #     expected: '1 2 3'
    not ok 4 - hash

    #   Failed test 'hash'
    #   at test_stringify.t line 28.
    #          got: '1'
    #     expected: '%hash'
    ok 5 - array_ref
    ok 6 - hash_ref
    ok 7 - object
    1..7
    # Looks like you failed 2 tests of 7.
not ok 2 - stringify

#   Failed test 'stringify'
#   at test_stringify.t line 32.
1..2
# Looks like you failed 1 test of 2.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/2 subtests 

Test Summary Report
-------------------
test_stringify.t (Wstat: 256 (exited 1) Tests: 2 Failed: 1)
  Failed test:  2
  Non-zero exit status: 1
Files=1, Tests=2,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.03 CPU)
Result: FAIL
mauke commented 3 weeks ago

No. Stringification (i.e. "convert value to string") is neither interpolation nor quoting.

Would you expect stringify(1 + 2*3) to be equal to "1 + 2*3"? If so, that's not a function. You want a quoting operator like qq(1 + 2*3) instead.

Similarly:

my %h = (a => 42);
say stringify(%{\%h});  # 1
say "%{\%h}";           # %{%h}

my $r = \%h;
say stringify(%{$r});   # 1
say "%{$r}";            # %{HASH(0x420b0ff0)}

There is no way for a normal function to distinguish between f(%{ \%hash }) and f(%{ $ref }) and to somehow recover the variable name (%hash) in the former case.

nicomen commented 3 weeks ago

@mauke I am mostly considered about consistency and least surprises. Sure, the string interpolation of a hash is a bit weird as it is, specicially compared to the magic for an array.

As you can see by the tests I added, stringify() is currently behaving exactly like concatenating to a string, if that's what stringification really means – that's fair. Then I have to alter my expectations and learn that there is a clear difference between interpolation and stringification.