[Note: Reply-to set to TT3 mailing list]
Morning all,
I'm in the process of re-factoring the Stash for TT3. There are a few
changes I am considering to the way TT binds to Perl code. Not major
things, but things that may be backwardly incompatible in certain ways.
In brief, current recommended practices will become mandatory and
most things should continue to work with little or no change.
There are a number of reasons for doing this:
* To remove some of the current ambiguities about passing and
returning values between TT templates and Perl code.
* To simplify the rules for writing Perl code that interfaces to TT
* To simplify the Stash code making it easier to maintain and port to
other implementations (like XS)
* To make possible some things that are currently impossible, like
using the undefined value in any meaningful way.
But first, the good news is that the old Stash will be available as
Template::TT2::Stash, along with most of the current TT2 modules,
providing full backwards compatibility if you want it. TT2 will effectively
become "archived" as Template::TT2 so that it's always there, warts and
all, for running your current TT2 setups alongside new TT3 ones.
Anyway, the purpose of this is to seek feedback on the changes and
assess the possible impact. That's where you people come in. :-)
Here's the overview of what I'm proposing:
* Parameters passed to Perl code will not be munged as they
currently are (TT2 merges named params into hash ref passed as
last argument)
* TT3 stash will call Perl code in scalar context by default
(TT2 uses list context).
* Perl code returning multiple items must in future return a reference
to list rather than just a list. This is the current recommended
practice but it's not universal.
* There will be some way to explicit denote list context, most
probably by adding '.list' after it: [% obj.method.list %]
* Variables can be set to the undefined value and Perl code can
return it without triggering an error.
* Errors should be reported by throwing exceptions via die().
Returning undef will no longer be considered an error.
Parameter Munging
-----------------
In TT2 we munge all named parameters passed to Perl subroutines,
methods and virtual methods, so that they appear in a hash array,
passed by reference as the last argument.
[% foo(a, b, c=>d) %]
This ends up as something like this:
&foo($a, $b, { c=>$d })
Any named parameters get moved to the hash array at the end.
[% foo(a, c=>d, e=>f, b) %]
Ending up as:
&foo($a, $b, { c=>$d, e=>$f })
The Perl code might do something like this to pluck the named
parameters from the end of argument list if the last item is a hash
reference.
sub foo {
my $params = @_ && UNIVERSAL::isa($_[-1], 'HASH') ? shift : { };
# $params has named params, @_ contains positional argse
...
}
That's all very convenient, but it's ambiguous if you pass a reference
to a hash array as the last argument, intending it to be a regular
positional argument.
[% a = 'foo';
b = { some=>'data' };
# ...later...
foo(a, b) # ==> &foo('foo', { some=>'data' })
%]
In this example we didn't want b to be interpreted as the set of named
parameters, but it has. We have to do something like this instead:
[% foo(a, b, { }) %]
Or we have to be a lot more explicit in how we write the Perl code,
and/or in defining the precise order and number of position arguments.
By then we have all but lost any benefit this magical parameter
munging might have brought us.
So it's a nice idea, but with too much action-at-a-distance to make it
reliable and robust. We have to remember to special-case any calls where
the last argument may be a hash reference, and that forces us to know more
about our data than we might care to (uniform access principle and
all that).
For TT3 I propose that we pass the parameters exactly as they are and
leave it up to the Perl code to do whatever is appropriate. That
means you will always need to pass named parameters at the end of a
list, rather than dotting them around anywhere you like. But this is
really just enforcing what has always been the recommended approach.
[% foo(a, b, c=>d, e=>f) %] ==> &foo($a, $b, c=>$d, e=>$f)
If you want to pass a reference to a list of named parameters then
you can do so like this:
[% foo(a, b, { c=>d, e=>f }) %]
Or this:
[% params = { c=>d, e=>f };
foo(a, b, params)
%]
You can write your Perl code to accept either a list of named parameters,
or a hash reference.
sub foo {
my ($a, $b) = (shift, shift);
my $params = @_ && UNIVERAL::isa($_[0], 'HASH') ? shift : { @_ };
...
}
Either way it works. You still have to be explicit in the order of
any positional arguments, but at least there is no ambiguity and no
hidden magic no catch you out. You might have to do a little more
work (or perhaps a little less) in your Perl code to massage the args,
but that's a small price to pay for more correct functionality, IMHO.
List and Scalar Context
-----------------------
This relates to the way that the stash calls a Perl subroutine or
object method which is bound to a template variable. Currently, TT
calls the sub/method in list context and folds multiple return values
into a reference to a list. This is to support code that does this:
return @items; # accepted grudgingly, not recommended
Instead of this:
return [EMAIL PROTECTED]; # recommended and highly applauded!
It can have unpredictable results if @items only has one item in it.
If one item is returned then you get the item, if more than one is
returned then you get a reference to a list of items. However, TT
subsequently Does The Right Thing to make single items look like lists
of one item so that you generally can't tell the difference.
For example, the FOREACH directive is happy to accept a single item,
or reference to list of items:
[% FOREACH a IN sub_that_returns_one_or_more_items %]
And scalar items can call list virtual methods - they get auto-upgraded
to a single element list on demand.
[% sub_that_returns_one_or_more_items.join(', ') %]
But there are problems. For example, if the items returned as a list
are references to lists, then it makes a big difference if you return
one or many:
@items = ([10, 20, 30]);
return @items;
vs:
@items = ([10, 20, 30], [40, 50, 60]);
return @items;
So my thinking for TT3 is to call all subs/methods in scalar context
by default. If you want to return a list of items then you will have
to return it as a reference to a list.
This won't make any difference to the majority of code that already
follows this recommended practice, but it will break code that
currently relies on this feature. ISTR that Class::DBI falls into
this category, and there may be others.
For those times when you do want list context, we can provide an
explicit way of indicating that. The Template::Stash::Context module
provides this in the form of .scalar post-op which tells the TT2 stash
to call in scalar context rather than the default list context:
obj.method.scalar # call obj.method in scalar context
So we could just switch that around for TT3 and look for a '.list'
following to trigger a switch to list context.
obj.method.list
That could still be a little ambiguous, as we might actually mean to
call obj.method in scalar context, and then convert it to a single
item list (the two may produce different results). But you could
code that kind of edge case explicitly if you really needed to:
obj.method.item.list
Or we may have some kind of prefix, magic symbol, extra syntax, or
something else that provides this kind of out-of-band information
about the dotops. Off the top of my head, it could be something
like:
obj.list:method
[EMAIL PROTECTED]
obj.*method
obj.(method)
But let's not get hung up on any new syntax for now. That's something
to think about for another day...
Returning Undefined Values
--------------------------
Another complication in TT2 is that we use a return value of undef to
indicate errors coming back from Perl code. If a second value is
returned after undef, then TT assumes this is an error message.
return undef; # anonymous error
return undef => 'an error occurred'; # specific error
Alas, our subroutine may return a list of items, some of which
(including the first) may be undefined.
@items = (undef, undef, 10, undef, 20, 30);
return @items.
Or we may have a subroutine/method that returns undef simply to indicate
that a particular value isn't defined, but that doesn't constitute an
error.
sub alias {
my $self = shift;
return $self->{ alias }; # may be undefined, no problem.
}
All of these get mis-interpreted as errors. Bad TT2!
So I'm thinking that we should allow code to return undef and we'll
treat it just like any other value. It currently isn't possible to
use undefined values in TT, but there are occassions when you might
want to pass undef into a Perl subroutine from TT, or explicitly set a
value to be undefined, rather than just setting it to an empty string.
For TT3 I propose we remove that limitation and recognise undef as the
valuable (er, I mean valueless) member of the data community that it
is (or isn't). TT3 should still handle undef values like it currently
does when it comes to printing them out: either print a blank string
and say nothing, or raise an 'undef' error, depending on how the
current configuration options happen to lie. But it will no longer
be an error for a variable to contain an undefined value or for Perl
code to return one.
Reporting Errors
----------------
If undef becomes a valid value for a variable, then we must also be
able to return it from a subroutine or method. In which case, we will
no longer be able to use this mechanism for reporting errors.
So this change would require that all errors are in future raised vie
die(). The code can either throw a structured Template::Exception or
just report an error message and we'll Do The Right Thing to catch it.
Again, this is the current recommended practice, and certainly what
all the plugins do, or should do. But any code that you currently
have that returns undef to indicate an error would break.
One quick work-around for this problem might be to have an explicit
'.assert' dotop, or something like it, which checks the previous value
for defined-ness, returning the value itself if it is defined or
throwing an error if not.
[% obj.method.assert %]
We could have a stash option which checks for undef return values and
throws them as errors to emulate the current behaviour. However, I
think this would actually give us the worse of at least two worlds.
The behaviour would become more ambiguous and you wouldn't be able to
work out what would happen from looking at the template or Perl code,
without also knowing the particular setting of some far-away Stash
option. And apart from anything else, the Stash code would become
even more complex, having to cater for both cases. That's definately
something that we're trying to avoid.
Summary
-------
So in summary,
* Parameter passed to Perl code will not be munged
* Perl code will be called in scalar context by default
* It must return a single item
* Otherwise call it with an explicit '.list' suffix (or similar)
* It may return undef as a valid value
* It should report errors using die()
Comments and discussion welcome, nay, positively encouraged. Directed
to the TT3 mailing list please.
A
_______________________________________________
templates mailing list
[EMAIL PROTECTED]
http://lists.template-toolkit.org/mailman/listinfo/templates