Global state in Web Applications: Catalyst’s stash.

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?

Comments

  1. Greg says:

    > I’m especially interested in learning about how strongly-typed FP stacks (HAppS?) confront this stuff.

    I can’t speak for all happstack users, but my usual solution for this kind of global state is a state monad wrapping IO; if you want truly global state, your state record holds an MVar, if you want per-request data your state record holds some other value which gets discarded at the end of the computation.

  2. Chris Eidhof says:

    There are frameworks like Seaside which allow you to store state and build continuations. Also, another example is Arc (a language by Paul Graham). I’m working on something similar for Haskell, the code is currently very alpha though. It’s at http://github.com/chriseidhof/thesis/

  3. Programmer says:

    Catalyst? State of the art? Not remotely (well, maybe in Perl land). It’s a typical “ducktwork is showing” (http://www.wall.org/~larry/pm.html) response to RoR.

  4. Ash Berlin says:

    To whoever claims to be a ‘Programmer’:

    > Catalyst? State of the art? … response to RoR.

    The first development release of Catalyst was on 2005-01-28, which while it was a few months after the first relase of Rails, was certainly well before Rails got popular, so calling Catalyst a response to RoR is just silly.

    Update: ash and lathos on #london.pm also discussed that Cat itself was a fork of Maypole (originally called Apache::MVC), first released 6 months before RoR’s first release.

  5. Dear Programmer..
    Catalyst forked from Maypole which was released before Rails, and had already won the Editors Choice award from Linux Journal before anybody had heard of Ruby on Rails.

  6. dima says:

    MVC Catalyst I liked, a very good framework