3

I'm staring at this code in perl LWP::Protocol.pm and I don't understand how the loop would ever exit:

 while ($content = &$collector, length $$content) {
     $content_size += length($$content);
     # more here
 }

In my interpretation, I am thinking this is a list of 2 elements in scalar context, so the result that the while loop is testing is the length of the list, which is always 2 and never false. How does this really work?

snoopyjc
  • 621
  • 4
  • 11
  • 2
    This is an odd piece of code, don't worry if you feel confused. Using the comma operator like this is unusual. I suppose someone struggled to come up with a loop condition that also refreshed the variable. It might be somewhat more obvious to write `while (length ${ $content = &$collector })`. For some weird reason this code returns a reference to a scalar -- some sort of premature optimization attempt perhaps -- and that is making things difficult. If they had not done that, you could just say `while (length $content = &$collector)`, or even just `while ($content = &$collector)` – TLP Feb 04 '22 at 10:17
  • 1
    https://metacpan.org/dist/libwww-perl/source/lib/LWP/Protocol.pm#L157 definitely a bad idea to use it in production code. – mpapec Feb 04 '22 at 11:11
  • @mpapec I just realized that using `&` will make the collector inherit the current `@_`, which seems to be `my ($self, $arg, $response, $collector) = @_;` I assume that the collector sub does not use `@_`, or else this would be a hot pile of steaming cowflop. It could propagate the erroneous `@_` to another sub though, affecting the `$self` object throughout the module. Hard to analyse how serious this is, though. – TLP Feb 04 '22 at 11:49
  • Unless you explicitly want to use the current `@_`, the correct way to write that code dereference would be `$collector->()`. I feel like this could be an unintended bug. – TLP Feb 04 '22 at 12:08
  • `$collector->(@_)` also comes to mind if parameters need to be passed. – mpapec Feb 04 '22 at 12:31
  • 1
    @TLP `&$collector()` would work too, though I do prefer `$collector->()` – ikegami Feb 04 '22 at 14:29

2 Answers2

4

Please have a look at the operator precedence in the documentation. There you'll see that = has a higher precedence than ,. This means first it does $content = &$collector due to the higher precedence of = compared to ,. Then it does length $$content.

Continuing with the documentation about the Comma Operator one will find:

Binary "," is the comma operator. In scalar context it evaluates its left argument, throws that value away, then evaluates its right argument and returns that value.

Thus the result in this case will be the right one, i.e. length $$content. This will then be used as the condition inside while.

Steffen Ullrich
  • 114,247
  • 10
  • 131
  • 172
  • So like I said, the list is 2 elements, the "$content=&$collector" element, and the "length $$content" element - where am I going wrong and why does the while ignore the first element of the list? – snoopyjc Feb 04 '22 at 06:13
  • 2
    @snoopyjc There is no list. Scalar comma operator is different than the commas in a list. See https://perldoc.perl.org/perlop#Comma-Operator – Shawn Feb 04 '22 at 06:19
  • @snoopyjc: I've updated the answer to include more details based on the comment from Shawn. – Steffen Ullrich Feb 04 '22 at 06:26
  • 1
    This answer mistaunderstands the OP's question. They wonder why `scalar( A, B )` is different than `@a = ( A, B ); scalar( @a )` – ikegami Feb 04 '22 at 07:20
1

I am thinking this is a list of 2 elements in scalar context

Correct.

Technically, the documentation calls it the comma operator when it's used in scalar context,[1] but the operator is still called list internally.

The only real problem with that statement is the choice of word element. Operators have operands (which are expressions), not elements. Element is used to refer to the members of data structures, which isn't applicable here.

so the result that the while loop is testing is the length of the list,

No.

You think some kind of list data structure gets produced, and it's then coerced into a scalar. This is a common misunderstanding, but it's not how context work at all. Scalar context doesn't coerce lists into their count; scalar context causes the operators to return a scalar from the start.[3] An operator in scalar context must produce exactly one scalar. And what that scalar is up to each individual operator.

A list/comma operator in scalar context doesn't return the length of anything. It evaluates each of its operands but the last in void context.[2] Then, it evaluates the last one in scalar context and returns the resulting scalar.

In other words,

scalar( EXPR, EXPR )

is similar to

scalar( do { EXPR; EXPR } )

  1. It also goes out of its way to call it a binary operator, when 1) people don't think of it a binary operator, the generated operator isn't a binary operator, and 3) it makes absolutely no difference if it's a binary operator or not. Weird.

  2. If the list is the last operand of a sub or the operand of a return, and if the sub is evaluated in scalar context, they will be evaluated in scalar context instead. This is a basically a WONTFIX bug.

  3. In fact, there's no such thing as lists in that sense even in list context. When we say "passes a list" or "returns a list", we simply mean "adds a number of scalars to the stack". No "list" gets created.

ikegami
  • 367,544
  • 15
  • 269
  • 518