SWYM (Say What You Mean) - Declarative Catalyst

Those of you who are not regulars on irc.perl.org or don’t hang out in the #catalyst channel might not be aware of this, but the discussion, planning and chanting was going on for a while. With the recent rise of Devel::Declare this discussion has flamed up again.

There are many people thinking about how to design this, and even some implementations are already around. I’d like to use this article to discuss my own take on how a declarative Catalyst might look like.

First: The Competition

I currently only know of Florian Ragwitz (from here on just called “rafl”) and John Napiorkowski, but I’m sure others are experimenting with this too. ++ to both of them, especially since I borrowed some of their ideas in my own draft.

Is this for real?

Yes. While all of this is of course still highly experimental and not available on the CPAN (which is a good indicator of its unfinished nature), you can get it from github, or if you just want to take a closer look at some examples, there is a prototype test controller in the test suite implementing the current functionality.

How it looks like

First, let’s take a look at a traditional Catalyst controller. CatalystX::Declarative is planned to allow declaration of components, views, etc. as well, but for the moment we only have controllers. Why? Because that’s the most code that we have to write. This is along of what you usually have:

  package MyApp::Controller::Foo;
  use strict;
  use warnings;

  use parent 'Catalyst::Controller';

  sub base: Chained PathPart('') CaptureArgs(0) { }

  sub foo: Chained('base') Args(1) {
      my ($self, $ctx, $id) = @_;
      $ctx->stash(foo_id => $id);
  }

  1;

Wouldn’t it be nice if we could instead say this?

  use CatalystX::Declarative;

  controller MyApp::Controller::Foo {

      action base under '/' as '';

      action foo (Int $id) under base {
          $ctx->stash(foo_id => $id);
      }
  }

While it might not look like it, this is still Perl 5. The controller construct is an extension of MooseX::Declare’s class keyword handler. IT automatically sets up Catalyst::Controller as base class if none was specified via extends.

The action keywords seem to work like methods, but are a bit more complicated. First of all: They don’t need blocks behind them. The base action above is just simply empty and provides nothing more than an anchor point.

The foo action additionally has a signature. This will be introspected when the actions are populated. That means the above will correctly only match on a single argument. Types are currently validated like normal methods. I’m not yet sure whether to keep it validating and provide a multi action keyword or to extend it so the action will only match when the arguments match the signature. This would mean that you would have to say

  action foo_int (Int $id) under base as foo { ... }
  action foo_str (Str $st) under base as foo { ... }

Which isn’t that nice, really. I’ll probably go with an adaption of multi method and do this instead:

  multi action foo (Int $id) under base { ... }
  multi action foo (Str $st) under base { ... }

This seems much nicer.

You might have already noticed, this appraoch to declarative controllers is completely based on :Chained actions. Everything that is not denoted as is final will have an implicit CaptureArgs.

You can also specify a slurpy parameter like this:

  action foo (@bar) under '/' is final {
      $ctx->stash(bar => join ', ', @bar);
  }

And Catalyst will know to expect a dynamic amount of arguments, the same as if you’d have specified it like this:

  sub foo: Chained Args {
      my ($self, $ctx, @bar) = @_;
      $ctx->stash(bar => join ', ', @bar);
  }

There are already some implemented syntax elements to declare:

  • action $name $signature?
  • under $private_path
  • as $path_part
  • is final
  • is private
  • with $action_role
  • isa $action_class
  • final

These were the rather basic ones. Paths can always be specified as string or as bareword if they follow the rules of Perl identifiers:

  action foo as bar;

is the same as

  action foo as 'bar';

This is more than TIMTOWTDI to me. It allows me to make an easy distinction between actions chaining to a local or an external action (including /):

  action base under '/';
  action foo under base;
  action bar under '/base';

Since one of the main differences between rafl’s and my approach seem be the ordering of specific parts of the syntax (namely the under keyword), I made action and under equals. This means you can write both

  action foo under base as 'myfoo';

and

  under base as 'myfoo' action foo;

and they will mean the same. The order of the other syntax elements are not important. They are extendable, so being enforced to provide them in a specific order would probably become a huge pain in the future.

Another issue rafl had was that he intended to use under in block format to bundle a bunch of actions under a common chain point. Turns out this wasn’t that hard to implement:

  action base as '';

  under base {

      action list   { ... };
      action create { ... };

      action object ($id) {
          $ctx->stash(object => $ctx->model('Foo')->get($id);
      }

      under object {

          action update { ... };
          action delete { ... };
      }
  }

In the above example, both list and create will chain off base, just like object which provides the chain base for update and delete.

At this point you, as well as I, might ave noticed that these declarations can get pretty long, and full of barewords. Here is an example:

  action foo (Int $x, Str $y) under some_base as two_foo is final { }

That’s declarative, but a bit hard to parse. Fortunately, you can throw in commas between the syntax elements:

  action foo (Int $x, Str $y), under some_base, as two_foo, is final { }

reads a lot nicer.

You can also customize your action by using with and isa. To add an ActionRole you’d simply say

  action foo under bar with OnlyMatchOnSundays { }

This will try to find an ActionRole fitting that name. In sequence, it would look for

  • A subroutine (like an alias or constant) named OnlyMatchOnSundays. This will be executed if found and its return vaule used as ActionRole name.
  • MyApp::ActionRole::OnlyMatchOnSundays, which would be the logical place if this ActionRole was provided by your application itself.
  • Catalyst::ActionRole::OnlyMatchOnSundays for roles you installed via CPAN or those you intend to go there.
  • A package fully named OnlyMatchOnSundays.

Besides ActionRoles, you can also specify ActionClasses:

  action end is private isa RenderView;

This will look for an ActionClass with the same naming rules as ActionRoles above, except that it will use the Action namespace instead of ActionRole. (The above usage of is private is the only exception to the “All actions are chained” rule.)

Besides all of the features required to declare actions, there is a big emphasis on readability and flexibility on writing, just like we know and love it from Perl. Since I often have rather large controllers with complex action chains setting up specific environments for the final actions, I wanted a way to have the final actions in the chains stand out. This is why I introduced the final keyword that can be used as normal syntax element or even as declarator (meaning at the beginning of the syntax) just like action and under:

  final action foo ($x) under bar { $ctx->response->body($x) }

  under bar, final action foo ($x) { $ctx->response->body($x) }

  action foo ($x) final under bar { $ctx->response->body($x) }

This should help those nasty endpoints to stand out visually if they need to.

Reaction

For those who were hoping for another Reaction post, give this a quick thought:

  use ReactionX::Declarative; # made-up name, doesn't exist yet

  controller MyApp::Controller::Foo {

      action bar, under '/base', is final, pushing SiteLayout {

          $vp->title('Foo Title');
          $vp->static_base_uri( $ctx->uri_for('/action')->as_string );
      }
  }

Questions? Feedback?

There are many opinions flowing around at the moment, and many people seem to have their own impressions and ideas on how a declarative approach would make their lives easier. If you have any questions or comments, they would be greatly appreciated.

No TrackBacks

TrackBack URL: http://www.catalyzed.org/mt/mt-tb.fcgi/28

1 Comment

| Leave a comment

Very nice.
Can't wait until this is CPAN-ready. :)

Leave a comment

All comments are moderated. Spammers don't waste your time

Sponsored By


Ionzero: Rescue your dev project.
OpenID accepted here Learn more about OpenID

Following

Not following anyone

Note to spammers: all comments are moderated. Don't waste your time