The Perl web framework Catalyst has, and I’m fairly sure this is in
common with many such frameworks in any programming language, a concept
of a store of data, scoped to the current request, called the “stash”.
I’ve mentioned a few times that “I don’t like it” and the reaction I get
suggests that I don’t understand it. So it’s a good time for me to be
ignorant in public so some clever people that do understand can
tell me what I’m missing ;-)
Global status
I say that the stash is “request scoped”. But really, for the lifetime
of the web request, it’s a big global pile of random data. Some things
would naturally seem to fit there: big things to do with global status,
like the logged-in user. So, for example, a base action might check for
POSTed login details, and for session cookies, and set
$c->stash->{user} to a MyApp::User object.
Then, further down the line, another action might want to read user
details, and can do so by retrieving it from the stash.
This may seem very convenient. But of course, being in a hash, we don’t
get certain protections. For example, should later actions be
able to read the user object? Should they be able to modify it?
What about typing? What if someone sets {user} to a String, or
an undefined value? It really seems like it might be better to model
this kind of global status as an object, for example:
use MooseX::Declare; class MyApp::Status { has 'user' => ( isa => Maybe[ MyApp::User ], predicate => 'is_logged_in', is => 'rw', coerce => 1, ); coerce 'MyApp::User' => from 'Str' => via { return MyApp::User->from_name($_) }; coerce 'MyApp::User' => from 'Int' => via { return MyApp::User->from_id($_) }; ... # other global status things }
This way we get lots of modern/enlightened/whatever Perl OO niceties
like coercion, and methods built for free, like is_logged_in.
We could also provide a custom accessor that didn’t allow the user to be
updated if it’s already been set, etc.
Passing information down the chain
I mentioned that a base action could set the logged in status near the
beginning of the request. In fact, this is a common pattern in many web
frameworks, with a whole pipeline of actions happening in order. For
example, Catalyst has “chained actions”, where a URL like
/client1/documents/invoices/23
might build a chain like this:
- login
- admin login
- client specific customizations
- document template setup
- invoice template setup
- fetch invoice number 23
It’s unlikely these days that you would write:
method handle client1_documents_invoices ($invoice_number) { $self->login or return; $self->check_is_admin or return; my %client_args = $self->do_client1_customizations; my %template_args = ( $self->document_template_setup(%client_args), $self->invoice_template_setup (%client_args), $self->fetch_invoice( $invoice_number ), ); $self->process_template( %template_args ); }
That would be ridiculous, as you’d have hundreds of such methods… one
of the major selling points of a framework is to allow you to combine
these little pieces flexibly and elegantly. So you end up with the
elements in the chain as separate actions… But how do we manage the
flow of data between these items (the %template_args and %client_args in
my contrived example above, for example) ?
We use the stash of course! So each action can read the values it needs
from the stash, and write things later actions will need back into it.
method an_action_in_a_long_chain () { my $foo = $c->stash->{foo}; my $bar = do_something_to( $foo ); $c->stash->bar( $bar ); }
Hurray! Except that you may have noticed that we’ve reinvented passing
formal parameters and return values using global data. Welcome to 1959,
enjoy your stay in COBOL!
And of course, if we get parameters via the stash, we lose the benefits
of typing. And we have to be really certain that the bit of global data
we want has been set by the time our action gets called. And, because
we have a global namespace, we have to hope that some other action in
the meantime hasn’t set the value to the wrong sort of data.
It also leads to us thinking about our data as singletons. When we
realise later that we need to call an action on two separate objects,
what do we do? We only have one global slot for the parameter name, so
we now have to set it twice (or localize it), and we have to squirrel
away the first return value before it gets overwritten (yuck)!
Spaghetti and action-at-a-distance may not be the intent of the stash,
but they do feel like the logical progression of the concept to me.
Template values
Perhaps the least heinous usage of the stash is as the final store of
data to be sent to the template. Each action in the chain will set some
information:
$c->stash = { # set by a navigation component breadcrumb => [ 'client1', 'documents', 'invoices' ], menu_items => [ 'save', 'edit', 'delete' ], # set by the login action username => 'osfameron', usertype => 'admin', # set by the invoice action invoice_data => $MyApp::Model::Invoice, ... };
Again though, what’s to prevent one action from stomping over the
other’s data? Will the template die if it gets a hash for
invoice_data instead of a MyApp::Model::Invoice
object? What happens if some data it was expecting never got set?
What’s the alternative? I suspect that a “widget” approach might be
saner, and I have to confess I still haven’t looked at Reaction yet:
perhaps that’s what I’m looking for?
How do other frameworks deal with this?
I’m not, by the way, bashing Catalyst in particular – it’s what I’m
using now, but the frameworks I’ve used or written in the past have also
had the features of pipelines of actions, and some kind of flattened
global state (whether it was a stash, or just a hash that was passed
down the pipeline). Given the problems I’m seeing, I can’t believe that
this is the final goal, or even the state of the art. So:
- What am I missing?
- How does your framework deal with these issues more elegantly?
(For that matter, how does Catalyst?) I’m especially interested in learning about how strongly-typed FP stacks (HAppS?) confront this stuff. - Where next?