Perl: the straw that broke the camels back

2014-03-30

Wrestling with a bug on a Friday afternoon I came across a 'peculiarity' of the perl language which left me quite taken back, the offending code was eventually

# return empty data unless our dict has values for keys 'foo', 'bar' and 'baz'
# WARNING: this code is broken, do not use.
return {} unless @data{qw/ foo bar baz /};

For those not familiar with this part of perl; this syntax is a hash slice [1] and will return a list of the values stored under those keys

For example

my %dict = ( a=>14, b=>12, c=>0 );
for my $val ( @dict{qw/ a b c/} ){
    print "$val\n";
}

will print the numbers 14, 12 and 0 (separated by newlines)

for the purpose of this blog post I am going to instead substitute in a function that returns a list

sub function{
    return (14, 12, 0);
}

Contexts:

We will need to know about perl's idea of contexts.

Simply put a context is a place where an expression occurs, in perl the 'type' of place has an effect on the meaning of the value, contexts can be thought of as 'the type of value expected'.

Perl has 3 types of contexts [2]:

Void context is simple, it does nothing with the value (but the evaluation can still have side effects)

3;

Scalar is a context where we want a single value

# here we assign a scalar value to a scalar variable
my $a = 4;

List context is one where we want a list of values

# here we assign a list value to an array variable
my @b = (14, 12, 0);

Mixing types and contexts:

Things start to get interesting when we learn about mixing types with contexts:

Everything in void context is ignored equally

foo(); # function call, return value is ignored
4 + 12; # evaluated to 16, value ignored
(14, 12, 0); # all ignored

Scalar values in scalar contexts are simply their value (3 is just 3)

In a scalar context an array is it's length

my @b = (14, 12, 0);
if( @b ){
    ...
}

is equivalent to

my @b = (14, 12, 0);
if( 3 ){
    ...
}

Example:

So far so good, let's look at an example demonstrating some of the things we have learned:

my @a = (14, 12, 0);

# array in scalar context => length
if( @a ){
    print "first\n";
}


sub function {
    return (14, 12, 0);
}

my @b = function();

# array in scalar context => length
if( @b ){
    print "second\n";
}

# list in scalar context => ???
if( function() ){
    print "third\n";
}

The output of this is:

first
second

Do you find this surprising?

I do.

Surprising results:

So what is going on here?

A simpler view:

if( (14, 12, 0) ){
    print "I won't print this\n";
}

if we run this with warnings enabled perl will helpfully tell us

Useless use of a constant (14) in void context at foo.pl line n.
Useless use of a constant (12) in void context at foo.pl line n.

Since if is a scalar context perl sees this as (roughly)

14;
12;
if( 0 ){
    print "I won't print this\n";
}

and in perl 0 is falsy, so the if statement is not entered.

Conclusion:

There were a few other things that compounded to make this bug very tricksy, but I will omit these as this post is already a bit long (read: rambly).

This was so horrifying as my model of computation doesn't match up perfectly with perl's.

That is to say I consider the expression (14, 12, 0) to be equivalent to the expression @a when it holds that value [4], but in perl these are fundamentally different.

Parting notes:

The following code will produce no warnings and omit no output:

#!/usr/bin/env perl
use strict;
use warnings;

if( (1, 0, 1, 1, 0, 1, 0, 1, 1, 0) ){
    print "hello there\n";
}



[1] Perl slice documentation http://perldoc.perl.org/perldata.html#Slices

[2] Perl context documentation http://perldoc.perl.org/perldata.html#Context

[3] Technically if is a boolean context, but this is really just a special case of scalar

[4] Of course I am only considering reading, as soon as we have mutation this similarity breaks down