It’s been a long time since I’ve written about Seaside here. So it’s time for a new post.
If you are a VA Smalltalk user and would like to get your feet wet with Seaside, you will find out quite soon that the lack of VA Smalltalk’s support for Continuations makes it harder to follow the Seaside tutorials than it would be using Squeak or Pharo.
While some people think that Seaside without Continuations is like Smalltalk without Blocks, I think that is simply not true. Most web sites in the wild (meaning non-Seaside) have never shown the behavior that Continuations show, and therefor a Continuation-based app will feel strange. And: the more AJAX you use in your application, the less relevant this whole backtracking thing becomes.
So, having said that, we should take a look at what you can and cannot do without Continuations.
There is a whole family of methods and classes that you cannot use. The most important ones being call: and its variants inform: confirm: etc. This also means that you can forget about the class WATask, which acts as a logic glue and makes heavy use of call:.
Instantiations advices you to use show:onAnswer: as a replacement for call:, which is at least part of the truth, becuase depending on what exactly a callback does, this may or may not work as you might expect. The use of show:onAnswer: is often referred to as “Continuation Passing style” because instead of having Seaside work its Continuation magic on your calls/answers, you hand the called component a block with the code you want it to execute once it answers.
But one step after the other.
What happens if you call: or show: a Component?
Leaving Continuations aside, what happens in the first place is quite the same: the calling component puts adds a WADelegation to itself. This WADelegation contains the next component which will be drawn in place of the Caller at the next render phase. In case of show: that’s all that happens, in case of call: there’s a lot more going on, like copying the stack and kick of a new “subroutine”. I’m not going to try to explain Continuations and leave that for people much more clever than I am.
In order to make my point, I’d like to introduce a tiny example with three components with which you can see what the differences between calling and showing are.
So let’s by implementing a first Component:
WAComponent subclass: #OfFirstComponent instanceVariableNames: 'answer ' classVariableNames: '' poolDictionaries: ''
With one class method:
registerAsSeasideRoot WAAdmin register: self asApplicationAt: 'ShowAnswer'.
which you should execute now to make our application show up in the Seaside startup page.
This component simply renders two links that either call or show another component:
renderContentOn: html html heading level: 1; with: 'This is the first Component'. html anchor callback: [self callSecondComponent]; with: 'Call the second Component'. html space. html anchor callback: [self showSecondComponent]; with: 'Show the second Component'. answer ifNotNil: [ html break. html paragraph: 'And this is what the component answered: ', answer.].
As you can see, we want to implement two separate methods for the two links. One to use call: and one to use show:onAnswer:. If you are on a platform that does not support Continuations (like the current VA Smalltalk version), you can skip the call: variant since it will simply throw an exception. If you use another Smalltalk platform, implement both and see what the difference is…
So let’s implement the two callback methods in OfFirstComponent:
showSecondComponent self show: OfSecondComponent new onAnswer: [:ans| answer := ans]. self show: OfThirdComponent new onAnswer: [:ans| answer := ans].
callSecondComponent answer := self call: OfSecondComponent new. answer := self call: OfThirdComponent new.
Now let’s implement to more Subclasses of WAComponent: OfSecondComponent and WAThirdComponent. Since they have nothing but a simple renderContentOn: method, I’ll only show you these:
OfSecondComponent>>renderContentOn: html html anchor callback: [self answer: 'Answer 1']; with: 'Answer 1'. html space. html anchor callback: [self answer: 'Answer 2']; with: 'Answer 2'.
OfThirdComponent>>renderContentOn: html html anchor callback: [self answer: nil]; with: 'No answer at all'.
You should now see what’s going on: in the first component, we can choose to either call or show the second and third component one after the other. Both will answer some text to the first component.
The call example is pretty straightforward:
OfFirstComponent calls OfSecondComponent before calling OfThirdComponent. So what you see is a sequence of OfFirstComponent, OfSecondComponent and OfThirdComponent in exactly this order. The contents of the instance variable answer fill be updated to the answer from OfSecondComponent before the third component gets called. In the end the answer to OfFirstComponent is ‘No Answer at all’ because OfThirdComponent will answer this string to the first Component. What this means is that is perfectly safe to cascade calls and work with their results in assignments.
The show:onAnswer: example shows a surprising behaviour:
When you click on ‘Show the second Component’, you will be presented the third Component first, then the second, and the answer of the second component will end up as the answer on the first component.
Why is that?
In contrast to call:, the method will not dive into new copies of the call stack when show:onAnswer is sent. This means that after the first show:onAnswer: the method will simply continue to run, and the second show:onAnswer: will immediately be executed.
So what is going on: The first Component adds two delegations to itself, the first being a delegation to OfSecondComponent, and the Second delegating to OfThirdComponent. At the next render phase it is the last delegation that wins, so the last delegation to OfThirdComponent is the one that takes effect. When OfThirdComponent answers, the delegation will be removed, and at the next render phase the delegation to OfSecondComponent is in effect and therefore the second component is shown after the third Compoonent.
So what are the rules of thumb for using show:onAnswer:?
- show:onAnswer: should always be the last thing you do in a callback, because what you do after it will be executed before the answer arrives
- never send show:onAnswer: more than once in a callback, unless you know very precisely what you are doing
- Don’t try to simulate a chain of call:s with show:onAnswer: your code will be a real mess to read and change
I hope this little code example together with my attempts to explain what’s going on help you understand the differences between call: and show:onAnswer:. At least I have given you a tiny piece of code to start playing with and dive deeper into the matter.
And when you play with this code, always remember you can always set breakpoints in Smalltalk/Seaside and look at what exactly is going on, and once you resume work, the web application simply continues to work. No restarting or anything.
Once you understand the difference between call: and show:onAnswer: you will find that working without Continuations is not too bad, even if some convenience methods are missing (but who wants to use inform: in a production application anyways?). The only bad taste in your throat may be that you cannot use WATask. And even that is probably not that much of a loss in an application which is composed of many components, like a side bar, menu bar and stuff. Using WATasks in such an application can make life more complicated rather than simplifying it.
Having worked on VAST and Seaside for quite a while now I must say I am not missing Continuations much. The only problems I encountered were mostly related to documentation and probably some code imported from other dialects.