Perl: Parentheses vs Brackets for array definition, why is one considered a scalar?

4.3k Views Asked by At

I was following this tutorial on the HTML::Template module for Perl. Here's the template:

<!--template2.tmpl-->
<html>
<body>
<table>
  <tr>
    <th>Language</th>
    <th>Description</th>
  </tr>
  <tmpl_loop name="language">
  <tr>
    <td><tmpl_var name="language_name"></td>
    <td><tmpl_var name="description"></td>
  </tr>
  </tmpl_loop>
</table>
</body>
</html>

And here's the CGI test program:

#!"C:\Strawberry\perl\bin\perl.exe" -wT
use CGI qw(:all);
use CGI::Carp qw(fatalsToBrowser);
use HTML::Template;

my @rows = (
  {
    language_name => 'C#',
    description   => 'Created by Microsoft'
  },
  {
    language_name => 'PHP',
    description   => 'Hypertext Preprocessor'
  },
  {
    language_name => 'Haskell',
    description   => 'Functional language'
  },
);

print header;
my $template=HTML::Template->new(filename=>'template2.tmpl');
$template->param(language => @rows);
print $template->output();

This fails with the following error: HTML::Template::param() : attempt to set parameter 'language' with a scalar - parameter is not a TMPL_VAR!

However, when I change the definition of @rows from using parenthesis to using square brackets(from my @rows=(...) to my @rows = [...]) the code works fine; it displays a table with the data.

As I understood from reading this article, the first form is an array defined from a list and the second one is a reference to an anonymous array. It's still not clear to me why the first form doesn't work. I'd appreciate you clarifying this for me.

3

There are 3 best solutions below

0
On BEST ANSWER

The tutorial you're following contains an error. The line

$template->param( language => @languages );

should be

$template->param( language => \@languages );

Why? Short answer: the right-hand side of the loop name you pass to param must be a reference to an array, not an array.

Long answer: When you pass arguments to a function or method, all of the arguments get expanded into one long list. This is a common source of mistakes for beginners. So in your code (and in the tutorial's code), you're not passing two parameters to the param method, you're passing four (one for the string 'language', and three for the elements of @languages.

Here's an example of this argument-list unraveling. If you have three variables as follows:

my $scalar = 'bear';
my @array  = ('rat', 'moose', 'owl');
my %hash   = (mass => 500, units => 'kg');

and you pass them to a function like so:

some_function($scalar, @array, %hash);

then the function will see eight arguments: 'bear', 'rat', 'moose', 'owl', 'mass', 500, 'units', and 'kg'! Perhaps even more surprising, the two sets of values from the hash might be passed in a different order, because hashes are not stored or retrieved in a determinate order.

Your solution of changing the parentheses to square brackets works, but not for a very good reason. Parentheses delimit lists (which can be stored in arrays or hashes); square brackets delimit references to arrays. So your square-bracket code creates a reference to an anonymous array, which is then stored as the first (and only) element of your named array @rows. Instead, you should either store the array reference (delimited with square brackets) in a scalar variable (say, $rows), or you should use parentheses, store the list in the array @rows, and pass a reference to that array to the param method (by using a backslash, as I did above with \@languages).

4
On

The param() method in HTML::Template takes pairs of arguments. The first value in the pair is the name of a template variable that you want to set and the second is the value that you want to set that variable to.

So you can make a call that sets a single variable:

$template->param(foo => 1);

Or you can set multiple variables in one call:

$template->param(foo => 1, bar => 2, baz => 3);

For reasons that should be obvious, the variable names given in your call to param() should all be variables that are defined in your template (either as standard tmpl_var variables or as looping tmpl_loop variables).

If you're setting a tmpl_loop variable (as you are in this case) then the associated value needs to be a reference to an array containing your values. There is some attempt to explain this in the documentation for param(), but I can see how it might be unclear as it just does it by showing examples in square (array reference constructor) brackets rather than actually explaining the requirement.

The reason for this is that the list of parameters passed to a subroutine in Perl is "flattened" - so an array is broken up into its individual elements. This means that when you pass:

$template->param(languages => @rows);

Perl sees it as:

$template->param(languages => $row[0], $row[1] => $row[2]);

The elements of your array are hash references. This means that $row[1] will be interpreted as a stringified hash reference (something like "HASH(0x12345678)") which definitely isn't the name of one of the variables in your template.

So how do we fix this? Well, there are a few alternatives. You have stumbled over a bad one. You have used code like this:

@rows = [ ... ];

This creates @rows an array with a single element which is a reference to your real array. This means that:

$template->param(language => @rows);

is interpreted as:

$template->param(language => $rows[0]);

And as $rows[0] is a reference to your array, it all works.

Far better would be to explicitly pass a reference to @rows.

@rows = ( ... ); # your original version

$template->param(language => \@rows);

Or to create an array reference, stored in a scalar.

$rows = [ ... ];

$template->param(language => $rows);

There's really nothing to choose between these two options.

However, I would ask you to consider why you are spending time teaching yourself HTML::Template. It has been many years since I have seen it being used. The Template Toolkit seems to have become the de-facto standard Perl templating engine.

0
On
language => @rows 

means

'language', $rows[0], $rows[1], $rows[2], ...

or

language => $rows[0],
$rows[1] => $rows[2],
...

You want

language => \@rows