Shawn M Moore

NAME

Jifty::Manual::Continuations - There And Back Again

DESCRIPTION

Continuations are a powerful concept in computer science -- in a nutshell, they allow you to store away the state of of the interpreter at any given point. More importantly, they allow you to return to that state at any later time, by calling the continuation with, and evaluation of that interpreter state will resume. They are a concept that first arose in LISP, but have implementations these days in Ruby, Scheme, Haskell, and Smalltalk, to name a few.

Thus, continuations allow you to preserve context, and return to it later. This is amazingly useful in web programming, which is limited to HTTP, which is an inherently stateless protocol. By passing around continuations, we can keep track of the context that got us to the current page.

While we can't construct full continuations at the interpreter level -- because Perl does not support them -- we can implement them at the level of HTTP requests. In technical terms, because they capture the control stack up from the beginning of a user's session, they are called delimited continuations.

Continuations are more useful than sessions. Sessions store information across browser windows. Sessions may also break in the presence of the back button, as the information displayed on the screen, and the information stored in the session may differ. Since continuations are immutable, and a new one is produced every time a change is made, the information displayed in the browser cannot get out of sync with the information contained in any associated continuation.

USING CONTINUATIONS

The simplest form of continuation use is in a template, using "tangent" in Jifty::Web, as follows:

    <% Jifty->web->tangent( url   => "/someplace",
                            label => "Go someplace") %>

This will create a link, which, when clicked, will store the current request into a continuation, and jump to the url /someplace. In the /someplace template, you can display information, and possibly have the user navigate between multiple pages before returning to the previous page:

    <% Jifty->web->return( label => "Back to whence you came" ) %>

Because this return does not carry a result value, you can think of it as a form of gosub. In comparison, ordinary hyperlinks are akin to goto statements.

Sometimes, it may be possible for the user to get to a location without having a continuation set. In that case, clicking on the "Back to whence you came" link will appear to do nothing -- which may be slightly confusing to the user. To remedy this, Jifty provides a way to specify a default location to return to:

    <% Jifty->web->return( to => "/default", label => "Go back" ) %>

Using return values

All of the above examples generate links, which means that they don't interact at all with actions. However, continuations can also be useful in creating complex multi-page actions.

Continuations are saved -- and the browser is redirected to the new URL -- just after all actions have been checked for validation but before any of them are run. This means that the new request has access to the full validation state of its parent's actions.

When a continuation is called, it first checks that all actions in the request were successful; if any failed, then the continuation is not called. If the request's actions were all successful, it merges together the Jifty::Results of current Jifty::Response with those in the Jifty::Response stored in the continuation. In doing so, parameters are mapped using Jifty::Request::Mapper. This makes it possible to return values from continuations into arbitrary places. For example:

    % my $action = Jifty->web->new_action(class => 'AddTwoNumbers');
    <% Jifty->web->form->start %>
    <% $action->form_field( 'first_number' ) %>
    <% $action->form_field( 'second_number',
           default_value => {
               request_argument => "number",
           }
       ) %>
    <% Jifty->web->tangent(
            url    => '/pagetwo',
            label  => 'Enter a second number',
            submit => $action
       ) %>
    <% Jifty->web->form->end %>

..and in /pagetwo:

    <% Jifty->web->form->start %>
    <input type="text" name="number" />
    %# We use as_button to tell Jifty that we want a button, not a link
    <% Jifty->web->return( label => 'Pick', as_button => 1 ) %>
    <% Jifty->web->form->end %>

..and assuming that AddTwoNumbers's take_action resembles:

    sub take_action {
        my $self = shift;
        my $one = $self->argument_value("first_number");
        my $two = $self->argument_value("second_number");
        $self->result->message("Got " . ($one + $two));
    }

The first page renders the entry box for the first number; the second input is hidden because Jifty notices that it is based on a mapped value: i.e., its default is set to {request_argument => "number"} instead of a plain scalar value.

Pressing the button validates the action but does not complete running it. At this point, the second_number argument to the AddTwoNumbers action has no real value -- however, it knows that it will, at the earliest possible opportunity, fill in its value from the number request parameter.

Jifty tangents to /pagetwo, where we enter and submit a number argument. Control then returns to the original page, where the request mapper maps the number value into the second_number argument of the AddTwoNumbers action, which then runs because it has received all arguments it requires.

Note that in the example above, the number argument is a plain request argument, not part of another action. More complex mappings are possible, including grabbing the results of or arguments to actions. This would make it possible, for instance, to use an action on the second page to validate the number before returning. This is slightly different from placing a validator on the AddTwoNumbers action, as that validator only gets called after control has already returned to the first page.

As dispatcher rules

The "tangent" in Jifty::Web function is context-aware -- if it is called in void context, it immediately saves the continuation and redirects to the new url. This is particularly useful, say, for authentication protection in before blocks:

    before '/protected' => sub {
        # shorthand for: Jifty->web->tangent( url => '/login' )
        tangent('/login') unless Jifty->web->current_user->id;
    };

And in the /login template:

    % my $action = Jifty->web->new_action(class   => 'Login',
    %                                     moniker => 'loginbox' );
    <% Jifty->web->form->start %>
    <% $action->form_field('username') %>
    <% $action->form_field('password') %>
    <% Jifty->web->return( to     => "/protected",
                           label  => 'Login',
                           submit =>  $action) %>
    <% Jifty->web->form->end %>

This establishes a button, which, if the Login action is successful, calls the stored continuation, or, lacking one, redirects to /protected.

As currently implemented, these redirect-from-dispatcher tangents works exactly like rendered-as-links tangents, in that when they return, all rules in the dispatcher are still executed from the start. Therefore the unless guard in the before '/protected' rule above is necessary to prevent recursion.

GORY DETAILS

Jifty's continuations are implemented in Jifty::Continuation, which is very little more than a place to store a Jifty::Request and its associated Jifty::Response.

The following diagram diagrams the stages of continuation handling, and their interaction with the dispatcher. For clarity, the page region handling code is included, but page regions do not currently interact with continuation processing.

                                /--------------\
          +---------------------v-+            |
          |........Request........|            |
          +-|-------------------|-+            |
            |                   |  RETURN  +---|---------------------+
    /----\  |                   \----------> Replace request with    |
    |  +-|--|-+ +==============+           | request in continuation |
    |  |.v..v.---> SETUP rules |           +-------------------------+
    |  |......| +==============+
    |  |..D...|
    |  |..I...| +~~~~~~~~~~~~~~~~~~~+      +-------------------------+
    |  |..S...---> Validate actions |      | Store current request   |
    |  |..P...| +~~~~~|~~~~~~~~~|~~~+ SAVE | and response, redirect  |
    |  |..A...|       |         \----------> to new scope and URL    |
    |  |..T...|       |                    +-------------------------+
    |  |..C...| +~~~~~v~~~~~~~~~~~~~+
    |  |..E...| |  Run actions      |      +-------------------------+
    |  |..R...| +~~~~~~~~~~~~~~~|~~~+ CALL | Merge results into the  |
    |  |......|                 \----------> continuation's results; |
    |  |......|                            | redirect to return URL  |
    |  |......| +==============+           +-------------------------+
    |  |......---> RUN rules   |
    |  |......| +=====|========+
    |  |......|       |
    |  |......|    +--v---------------+
    |  |......|    | Show templates   |
    |  |......|    +-------|----------+
    |  |......|            |
    |  |......|    +-------v----------+
    |  |......|    | Show page region ---------------------\
    |  |......|    +------------------+                    |
    |  |......|                                            |
    |  |......| +==============+                           |
    |  |......---> AFTER rules |                           |
    |  +------+ +==============+                           |
    |                                                      |
    \------------------------------------------------------/

As shown in the diagram above, there are three different operations that continuations use. The first is SAVE, which is triggered by the query parameter J:CREATE. Continuations are saved after validating actions; the continuation itself is attached to the user's session object.

The current saved continuation is automatically preserved across requests. When the time comes to call the continuation, the CALL operation is performed; this is usually triggered by the presence of the J:CALL query parameter. This causes the stored request to be query-mapped using Jifty::Request::Mapper, but using the current request and response (not the continuation!) as the sources for mapping values. Then, the result objects are merged, with results from the stored response taking precedence. This new mapped request and new merged response are formed into a new continuation.

In order to ensure that the browser's URL matches the URL of the request in the continuation, Jifty then does a redirect to the URL of the request stored in the continuation, starting the last continuation operation, the RETURN. When Jifty detects the RETURN operation, most often by the presence of J:RETURN, it loads the continuation and reads the stored request and response into the current request and response.




Hosting generously
sponsored by Bytemark