Brian's Waste of Time

Fri, 22 Apr 2005

Modal Transformations

Chatting with Santiago earlier we kicked around some ideas on doing modal webapps in ~pure java. Getting back-button compatible behavior isn't too bad by using a StateRegistry type system and requiring any view state be stored in a registerable component.

Getting linear style scripting is a lot trickier though, let's say we want this flow:

def request_free_icecream()
  name = ask_for :name
  address = ask_for :address

  unless blacklist.check_for_idjits(:name => name, :address => address )
    send_icecream_to(name, address)  
  else
    return "rejected"
  end
  return "accepted"
end

In this case ask_for(symbol) entails a request/response (possibly multiple if validation fails). The flow of what is going on is easy to see. Now, modeling this in a typical webapp might be:

def request_free_icecream()
  @session['icecream-request'] = IcyAccumulator.new
end

def set_name
  accum = @session['icecream-request']
  accum.name @params['name']
end

def set_address
  accum = @session['icecream-request']
  accum.name @params['address']
  finished()
end

def finished
  accum = @session['icecream-accumulator']
  unless blacklist.check_for_idjits(:name => name, :address => address )
    send_icecream_to(accum.name, accum.address)  
    redirect_to :controller => :globals, :action => :accepted
  else
    redirect_to :controller => :globals, :action => :rejected
  end
  @session['icecream-accumulator'] = nil
end

Now, aside from the fact that the second example actually works (unlike the first which is a variety of pseudocode called "ideacode"), it is way harder to follow what is actually happening. Nothing new here, modal webapp proponents have been flogging this horse for years. Now, let's pretend the above code snippets are Java, and as we know we cannot use the standard tool of continuations to build this out, how can we do it?

One option, which Torsten has done, is to transform the code at the bytecode level to emulate continuations. This is the ~classic way, but isn't the only way. It is a bit of a pita to do well, I think (I haven't tried, have just watched people doing it) -- probably because java bytecode doesn't lend itself to CPS style transforms.

What we need to do is effectively transform the first snippet into the second snippet. To do this it needs to be split into the following chunks:

... {
  invoke ask_for :name
}

... {
  assign result to :name
  invoke ask_for :address
}

... {
  assign result to :address
  unless blacklist.check_for_idjits(:name => name, :address => address )
    send_icecream_to(name, address)  
  else
    return "rejected"
  end
  return "accepted"
}

Which splits the code into the actions in the second code example. Need to be able to match the points at which control flow goes back to the user (ie, send a response and wait) and transform "what comes next" from a thread of execution into a function (functor) which can be invoked, so to look at it again, we would want a transform along the lines of:

one = functor do
  name = ask_for :name
end

two = functor do
  address = ask_for :address
end

three = functor do
  unless blacklist.check_for_idjits(:name => name, :address => address )
    send_icecream_to(name, address)  
  else
    #return "rejected"
    return_result "rejected"
  end
  #return "accepted"  
  return_result "accepted"
end

We now have three functors (some arm waving, bear with me) which encapsulate the flow we described. We will presume "correct closures" as Matz describes them (and groovy implements now, but ruby doesn't yet AFAIK), where the name and address variables are available to the functor being created and assigned to three (right now they'd be undefined in ruby as they are scoped to the box and don't fall through to the outer scope).

Now, we've hit the point where we need to see what ask_for actually looks like. I don't know. It would need to set up a page, fetch the results, and return the request params as an array. Probably it maps the argument to a view of some sort. This is all very cocoon.sendPageAndWait(..)-ish. The key point is that it passes through a single point to ask for a view rendering and announce it wants the results back. You can pinpoint what this chunk of code is.

It is a definable pointcut =)

Now, one of the (few) accepted patterns in AspectJ is a functor transform as I call it, or a worker object creation pattern as Ramnivas calls it. His name is better, but I think in terms of mine anyway (tangent, I am really looking forward to Raminvas's next book!). In this case you need to match the entrance and exit of the flow blocks, which can be nicely demarcated by naming conventions and calls to the "I need a request cycle" function.

It helps if each functor knows its "next", so we can alter it to create:


sequence = functor do
  name = ask_for :name
end

sequence = sequence.next = lambda do
  address = ask_for :address
end

sequence = sequence.next = lambda do
  unless blacklist.check_for_idjits(:name => name, :address => address )
    send_icecream_to(name, address)  
  else
    #return "rejected"
    return_result "rejected"
  end
  #return "accepted"  
  return_result "accepted"
end

Where sequence is the name of our functor, and each one knows its next. There is an awkward bit where the next= receives a proc instead of a functor, this is because it needs to act as a functor generator so that multiple calls to it create new functor instances. Until really implemented, this is just a guess on my part though.

Now, I can see in my head implementing this transform via AspectJ, but it'll take several hours at a minimum to go from rubyesque ideacode to Java/AspectJ implementation, so I am going to put that off. Pretend we have it, and have a useful Continuable implementation which is the functor stuff in the ideacode. Heck, the transform to a worker object may be viciously hard in this case, is just idea stuff right now.

We need to store these continuables in the session, probably under a map just like the continuations in FlowScript, and be able to call back to them based on continuation ids, we would need some convenient mechanism for managing them. That is a worry for another day -- right now we will treat it like naive C memory management (ie, none -- don't let sessions live long). Probably a sweeper-like system will make the most sense, but that is just intuition.

What we kinda did was kinda described a kinda implementation of a (broken and very limited) form of the continuation passing transform via aspects.

1 writebacks [/src] permanent link