As may be obvious, the first Perl templating tool I ever used is Mason. Because of that, for a long time, I looked at TT but I never used it. I had more or less dismissed it because I didn't see the point of a tool that provided a separate language from Perl. However, at both my last and current day jobs, we use(d) TT. Now I actually have solid reasons for not liking it, as opposed to vague thoughts that it "isn't right". For the record, I've also used HTML::Template in the past on a project, and that has helped inform my thinking on templating systems in general, and TT (and Mason) in particular.
I think the fundamental problem I have with TT is that it is a powerful language that is not Perl, a language with a few fundamental design flaws. There are other aspects of it that bother me, but these are mostly smaller issues of the API which are correctable, and Mason has similar issues, so I can't fault TT in those cases.
The single most pain-causing aspect of TT, IMO, is its variable scoping. Basically, variables in TT have a global scope, with some localization (ala Perl's
local) available. However, as Perl 5 demonstrates, lexical scope is a key feature for organizing code and preventing hard-to-debug action at a distance.
I suspect that this wouldn't be a problem if TT didn't also support setting variables. For example, with HTML::Template, most variables are global. This isn't likely to cause a problem, though, because there's no facility for setting variables in a template. This means that no matter how deep into a set of nested templates I may get, if I want to determine the source of a variable, I only need to look at one place.
Contrast this with TT. In TT variables, can be passed in the initial call to
Template->process(). But they can also be set in the templates themselves. To make things worse, a variable, once set, is available not only in the current template, but any templates that are included from that one.
[% SET size = 'large' %]
The size is: [% size %]
[% INCLUDE template2.tmpl %]
And in template2.tmpl:
The size is still: [% size %]
The "size" variable will be available in template2.tmpl as well as template1.tmpl.
This means that trying to track down where a variable was set can quickly become very difficult. To compound matters, my experience suggests that this causes people to simply use
INCLUDE without passing any arguments to the called template.
This is one area where Mason gets things exactly right. In Mason, a template (a "component" in Mason-speak) is very closely analogous to a Perl subroutine. The variables in a component are all lexically scoped to that component. If you want to use globals, you have to do so explicitly (
$My::Global::Var). When you call another component, it cannot see any variables passed to or created in the caller (with one fairly DWIM-y (and very clearly exceptional) exception). If you want a component to see some variables, you pass them as arguments, just like a Perl subroutine.
Even worse, in TT there are two ways to include another template. In my examples so far, I've used the
INCLUDE directive. Unfortunately, because of the way this directive works, it can be a bit slow. However, there is another, faster directive that can be used to include another template,
PROCESS makes the whole variable scope issue much, much more problematic. Basically, when a template is include via
PROCESS, it effectively shares the variable "stash" with its caller. This sharing continues down any number of nested
If I write something like
[% PROCESS template2.tmpl color = 'blue' %] then not only is "color" available in template2.tmpl, it's also now set in the caller! Even worse, if template2.tmpl sets any variables internally, they will be set in the caller. If template2.tmpl then uses
PROCESS to include template3.tmpl, variables set in template3.tmpl are visible in both template2.tmpl and template2.tmpl, and so on and so on.
Needless to say, this ability for the callee to overwite the caller's variables is a rich and deep source of bugs. I've experienced this more than once, and debugging these problems is painful. Of course, the obvious (partial) solution is to always use
Unfortunately, this seems to just be a lot slower than using
PROCESS . Back at Socialtext, we spent some time profiling the app, and found that a large amount of our time was being spent in
Template::Stash. Once we started switching uses of
PROCESS, our app got quite a bit faster, and
Template::Stash disappeared from the profiling. Of course, the code also became more fragile as a result.
There's many other things that bug me about TT, but I'll only mention a few of them, briefly. First, TT does enforce any sort of strictness in variable names, and does not issue warnings on undef. That means that if I typo a variable name in a substitution, or elsewhere, I simply get no output, and no warning of why. I know there is a mode that causes TT to die on undef, but that has its own problems (not good for production, for one). Perl's combination of strict variable names and warnings on undef can really help track down bugs. Why not emulate this?
The other problem is related to this one. If you pass a blessed hashref object to TT, and typo a method name, ala [% object.obejct_id %], TT "helpfully" tries to look up the typoed method name, "obejct_id" in the hash reference. Of course, it won't exist, and so it will be undef, which TT "helpfully" (and silently) uses in the substitution. This too has sapped hours of my life in debugging. However, I suspect that this problem, more than the others, could be fixed relatively easily. I've just got to find my supply of round tuits!
And I often desperately miss destructuring assignment (aka
my ($x, $y) = @point), though this can be worked around by returning a hashref instead of an array(ref).
I'm not going to talk much about the arguments for TT. Most of them seem to be about non-programmers editing templates. My experience says that non-programmers handle Mason just fine. But if you don't believe me, I'd suggest that non-programmers will do much better with HTML::Template than TT or Mason, anyway.
I think the fundamental problem with TT is that it is a powerful language, not too dissimilar from Perl, but not really similar enough. It provides plenty of rope for self-hanging, but the shotgun blast doesn't quite blow your foot off it you so desire, to mix two common Perl metaphors. Personally, I'd rather strangle sans foot with Mason, or if I must, stick a cork on my fork with HTML::Template.
Update: Please also read A brief followup to "Why I don't like TT".
1. My guess is that one of the following occurred. Either
INCLUDE came first, and was too slow, and thus
PROCESS was created, or perhaps
PROCESS was too bug-inducing, and therefore
INCLUDE was added. Either way, I wish
INCLUDE was the only option, and was faster