Use of parentheses with a block argument in grep produces unexpected outcome
Asked Answered
P

3

5

The first print of the following code does not print the expected result although the subroutine is passed as the block argument. It is expected to print 1 but print's 2. What is the difference between the first and second print statements?

my @arr = ("hello", "world");
print scalar(grep (sub {return "hello" eq $_}, @arr)); # return 2, not expected

print scalar(grep {"hello" eq $_} @arr); # return 1 as expected
Plumy answered 10/4, 2023 at 21:56 Comment(3)
I don't believe anyone has ever said these two are equivalent for grep. The sub codeword creates a code reference, but you're not evaluating the sub code. As near as I can tell, its just being used as the stringified value. Which is to say that it evaluates to "true" for all iterations of grep.Lyse
@TLP: Code refs and refs in general are always true, yes. But it's certainly not stringified. Probably numified, since it's a Boolean context ( a special case of Scalar context), but these are technicalities.Bonne
even no numification: "The Boolean context is just a special kind of scalar context where no conversion to a string or a number is ever performed."Bonne
T
6

Perl's syntax for its "functions" that take a block argument is a bit weird, and it's one of my Perl annoyances. There are some things that are just weird because that's how Perl does it:

grep {...} @array;   # no comma, block argument

grep $_ == 4, @array # comma, expression argument

Adding the sub doesn't look like a block argument to Perl simply because that's not the way that Perl parses things:

grep sub { $_ == 4} @array # missing comma, expression argument, compilation error

grep sub { $_ == 4} @array # comma, expression argument

But this works when you use this special block form with the sub left off, and Perl knows how to parse these special cases because Perl knows how to parse these special cases:

$ perl -MO=Deparse -e 'my @f = grep { $_ == 4 } @array'
my(@f) = grep({$_ == 4;} @array);
-e syntax OK

$ perl -MO=Deparse -e 'my @f = grep $_ == 4, @array'
my(@f) = grep(($_ == 4), @array);
-e syntax OK

$ perl -MO=Deparse -e 'my @f = grep sub{$_ == 4}, @array'
my(@f) = grep(sub {
    $_ == 4;
}
, @array);
-e syntax OK

$ perl -MO=Deparse -e 'my @f = grep sub{$_ == 4} @array'
Array found where operator expected at -e line 1, near "} @array"
    (Missing operator before  @array?)
syntax error at -e line 1, near "} @array
"
-e had compilation errors.

That's just how it is. I wish Perl had a more general idea of anonymous functions, and that's one of the things that Raku addressed. I think Ruby did a nice job with optional blocks, too.

Now, let's make our own function, f with a block argument by using prototypes (which is more often than not a the best idea). The situation is slightly different (maddening, I know) for a user-defined function than Perl's builtin stuff:

Give f a block, no problem:

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f {137}'
sub f (&) {
    $_[0]->();
}
print f(sub {
    137;
}
);
-e syntax OK

Give f a anonymous sub, no problem:

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f sub {137}'
sub f (&) {
    $_[0]->();
}
print f(sub {
    137;
}
);
-e syntax OK

But, use parens and Perl thinks the block is an anonymous hash, even if you try to trick Perl into seeing it as a code block:

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({137})'
Type of arg 1 to main::f must be block or sub {} (not anonymous hash ({})) at -e line 1, near "})
"
-e had compilation errors.
sub f (&) {
    $_[0]->();
}
print &f({137});

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({137;})'
syntax error at -e line 1, near ";}"
-e had compilation errors.
sub f (&) {
    $_[0]->();
}

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({return 137})'
Type of arg 1 to main::f must be block or sub {} (not anonymous hash ({})) at -e line 1, near "})
"
-e had compilation errors.
sub f (&) {
    $_[0]->();
}
print &f({(return 137)});

And sometimes that's just the way it is.

Terrenceterrene answered 11/4, 2023 at 8:43 Comment(0)
P
8

grep takes either a BLOCK or an EXPR.

In your second (working) formulation, you're providing a block, which evaluates with the local $_ value as expected.

In your first formulation, you're providing an expression. The expression happens to be an anonymous subroutine with $_ bound in it for each element being grepped, but the subroutine itself is not evaluated.

You can see what's going on more clearly if you were to map rather than grep the input list:

> print "[", join(",", map (sub {return "hello" eq $_}, @arr)), "]\n";
[CODE(0x14d82bdc8),CODE(0x14d82bdc8)]

> print "[", join(",", map {"hello" eq $_} @arr), "]\n";
[1,]

You could of course evaluate the ephemeral subroutine as part of the grep expression, but the syntax gets very beautiful very quickly:

print scalar(grep ((sub {return "hello" eq $_})->(), @arr)); # returns 1
Paigepaik answered 10/4, 2023 at 22:12 Comment(1)
For completeness, if the OP wanted to use a predefined function as EXPR he could also write grep foo(), LIST or grep {foo()} LIST or grep $code_ref->(), LIST ... and so onBonne
T
6

Perl's syntax for its "functions" that take a block argument is a bit weird, and it's one of my Perl annoyances. There are some things that are just weird because that's how Perl does it:

grep {...} @array;   # no comma, block argument

grep $_ == 4, @array # comma, expression argument

Adding the sub doesn't look like a block argument to Perl simply because that's not the way that Perl parses things:

grep sub { $_ == 4} @array # missing comma, expression argument, compilation error

grep sub { $_ == 4} @array # comma, expression argument

But this works when you use this special block form with the sub left off, and Perl knows how to parse these special cases because Perl knows how to parse these special cases:

$ perl -MO=Deparse -e 'my @f = grep { $_ == 4 } @array'
my(@f) = grep({$_ == 4;} @array);
-e syntax OK

$ perl -MO=Deparse -e 'my @f = grep $_ == 4, @array'
my(@f) = grep(($_ == 4), @array);
-e syntax OK

$ perl -MO=Deparse -e 'my @f = grep sub{$_ == 4}, @array'
my(@f) = grep(sub {
    $_ == 4;
}
, @array);
-e syntax OK

$ perl -MO=Deparse -e 'my @f = grep sub{$_ == 4} @array'
Array found where operator expected at -e line 1, near "} @array"
    (Missing operator before  @array?)
syntax error at -e line 1, near "} @array
"
-e had compilation errors.

That's just how it is. I wish Perl had a more general idea of anonymous functions, and that's one of the things that Raku addressed. I think Ruby did a nice job with optional blocks, too.

Now, let's make our own function, f with a block argument by using prototypes (which is more often than not a the best idea). The situation is slightly different (maddening, I know) for a user-defined function than Perl's builtin stuff:

Give f a block, no problem:

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f {137}'
sub f (&) {
    $_[0]->();
}
print f(sub {
    137;
}
);
-e syntax OK

Give f a anonymous sub, no problem:

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f sub {137}'
sub f (&) {
    $_[0]->();
}
print f(sub {
    137;
}
);
-e syntax OK

But, use parens and Perl thinks the block is an anonymous hash, even if you try to trick Perl into seeing it as a code block:

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({137})'
Type of arg 1 to main::f must be block or sub {} (not anonymous hash ({})) at -e line 1, near "})
"
-e had compilation errors.
sub f (&) {
    $_[0]->();
}
print &f({137});

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({137;})'
syntax error at -e line 1, near ";}"
-e had compilation errors.
sub f (&) {
    $_[0]->();
}

$ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({return 137})'
Type of arg 1 to main::f must be block or sub {} (not anonymous hash ({})) at -e line 1, near "})
"
-e had compilation errors.
sub f (&) {
    $_[0]->();
}
print &f({(return 137)});

And sometimes that's just the way it is.

Terrenceterrene answered 11/4, 2023 at 8:43 Comment(0)
K
4

grep doesn't take a subroutine. In fact, it's not even a real function[1]. grep can be called in one of two ways, and they really only differ syntactically: With an explicit block, or with an expression argument.

grep {"hello" eq $_} @arr;
grep("hello" eq $_, @arr);

You've already figured out the first form. For the second one, the expression you pass as the first "argument" to grep gets evaluated, not only once, but once per element of the array.

print scalar(grep (sub {return "hello" eq $_}, @arr));

In the example you gave, the expression argument is sub { return "hello" eq $_ }. That expression will get evaluated (the expression itself with be evaluated; I did not say the subroutine will get called. The subroutine will never get called) once per list element. grep returns any elements for which the block returns true, and "true" is defined by Scalar values:

A scalar value is interpreted as FALSE in the Boolean sense if it is undefined, the null string or the number 0 (or its string equivalent, "0"), and TRUE if it is anything else.

And a subroutine is not undefined, the null string, or the number zero, so a subroutine is a true value. Hence, every element of the input list satisfies the (trivial) block.


[1] Many of the built-in Perl functions in the confusingly-named functions page are not real functions and are in fact language keywords. sort is an infamous example. There is no way to write a function in pure Perl that behaves like sort (in that it can take a block, a subroutine name, or neither).

Karlotta answered 10/4, 2023 at 22:13 Comment(2)
Re "Many of the built-in Perl functions in the confusingly-named functions page are not real functions", Actually, none of the functions in perlfunc are <strike>functions</strike>subroutines. They're all operators. This is listed right at the top of the page: "The functions in this section can serve as terms in an expression. They fall into two major categories: list operators and named unary operators." (Technically, there's also at least one nullary operator.)Levana
Re "There is no way to write a function in pure Perl that behaves like sort" Well this is true for all built-ins with no prototype - like sort - because their special syntax needs special parsing. But the rest -and there are many- can actually be overridden with "real" functions. But I fail to see how this is relevant here with grep not accepting a code-ref. Some other languages have such a "lambda" interface, Perl has a different one. That's semantic, not syntax.Bonne

© 2022 - 2024 — McMap. All rights reserved.