I have a question: I'm confused about how to use with-flow/yield/answer
in a real-life example. Let's take the weblocks-demo code:
(defun initial-page (k)
"Renders the initial page."
(with-html
(:div :style "text-align: center; margin-top: 25em;"
(:p :style "font-style: italic;"
"Roses are red," (:br)
"Violets are blue," (:br)
(:a :href "http://en.wikipedia.org/wiki/Steve_Russell" "Steve Russell") " rocks," (:br)
"Homage to you!" (:br))
(render-link
(lambda (&rest args)
(declare (ignore args))
(answer k))
"Next"
:ajaxp nil))))
(defun init-user-session (comp)
"Initializes the user session."
(init-temp-store)
(with-flow (composite-widgets comp)
(yield #'initial-page)
(setf (widget-args comp)
`(:prewidget-body-fn render-header))
(yield (list (make-instance 'flash :messages
(list "Welcome to weblocks demo - a
technology demonstration for a
continuations-based web framework
written in Common Lisp."))
(make-main-page)))))
Now, what if I wanted the initial page to contain a number of links to
other "pages"? I thought I could simply call render-link in initial-page
with lambda functions containing yield. But, in order for that to work,
you have to wrap the whole thing in a with-flow statement and specify
the component you want to replace. Well, the initial-page function does
not get the 'comp' parameter, so how do I know which component I want to
replace? Composite-widgets of the root component?
I believe a more complicated example would help greatly --
it is too easy to misuse continuation-based frameworks. I think the
weblocks-demo has too much code dealing with header/footer generation,
and not enough code pertaining to widgets and flows.
My dream example would render an initial page with a bunch of action
links, each of which would enter a separate flow (say, a two-step
process). Completion of a process should take me back to the home page.
And another question: how do I make the following work?
(with-html (:p (render-link #'some-function (:img :src "somewhere.png"))))
I worked around the problem by wrapping the :img in a
(cl-who:with-html-output-to-string), but there must be a better way?
--J.
> Now, what if I wanted the initial page to contain a number of links to
> other "pages"? I thought I could simply call render-link in initial-page
> with lambda functions containing yield. But, in order for that to work,
> you have to wrap the whole thing in a with-flow statement and specify
> the component you want to replace. Well, the initial-page function does
> not get the 'comp' parameter, so how do I know which component I want to
> replace? Composite-widgets of the root component?
The whole with-flow/yield paradigm is implemented because it's
possible to have multiple flows on a single page. You can have two
widgets on the same page go into continuation flows. For an example,
think of two independent "wizard" style widgets on the same page that
present you with a set of choices and allow you to click "Next". When
you click "next" on each one, you want 'yield' to replace the proper
wizard, not the whole page.
The 'comp' argument passed to 'init-user-session' is the root
composite - you can obtain it by evaluating (root-composite) macro at
any time in the request. So you don't need to pass 'comp' explicitly.
If you are replacing the whole "page" (i.e. everything the user sees),
you don't really need with-flow/yield. You can just have a series of
links on the initial page and when the user clicks on any of these
links you can simply call 'do-page' with a widget (just be sure your
callbacks are defined with lambda/cc or defun/cc). In this case
'do-page' will automatically replace the root with your widget, and
when it calls 'answer' the initial page will be placed into root
again. This way you don't have to bother with the with-flow macro, and
setting it to (root-composite).
> I believe a more complicated example would help greatly
I agree. Once I check in the changes I'm working on now, I'll focus on
a real world application designed to (hopefully) make money. I will
likely open source it so that people can use it as a real world
example of weblocks usage.
> (with-html (:p (render-link #'some-function (:img :src "somewhere.png"))))
Currently render-link is a function that expects a string for the link
contents. You'd have to hack it a little to accept cl-who code (it
would have to be turned into a macro). If you do this, please send me
a patch :)
Also, please send more feedback if you have any, it is very useful.
Regards,
Slava Akhmechet
I've been reading about this DSL for UIs and I'm very curious about
it. Mostly because I've been heading the same way when designing web
apps several years ago -- and because so far I really like your approach
to designing software. It is difficult to find a good balance between
being strict and allowing leeway, and so far I think weblocks is heading
in the right direction. As an example, I really like the simple fact
that not everything has to be a widget in order to get rendered.
Thanks for the explanation, makes perfect sense. However, I'm stuck
again: I can't figure out why the following does not work:
(defun my-home-page ()
(with-html (:p (render-link (lambda/cc (&rest args)
(declare (ignore args))
(do-page #'my-other-page))
"Link"
:ajaxp nil))))
(defclass parameters ()
((id)
(total-value :accessor total-value :initarg :total-value :initform nil :type integer)))
(defun/cc my-other-page (k)
(let ((my-params (make-instance 'parameters :total-value 42)))
(make-instance 'dataform :name (gensym) :data my-params)))
(defun init-user-session (comp)
(setf (composite-widgets comp) (list #'my-home-page)))
Clicking on "Link" results in an empty template page, the dataform
widget does not get rendered. However, if I replace my-other-page with
this:
(defun/cc my-other-page (k)
(with-html (:p "Other Page")
(:p (render-link (lambda (&rest args)
(declare (ignorable args))
(answer k))
"Go Back"))))
... things work just fine. I can't figure out why the dataform widget
does not get rendered. Is there a way to debug these kinds of problems?
It's frustrating, because one ends up with an empty page and apart from
trial and error there is little to be done.
Also, I don't think I understand fully when I need to use defun/cc and
lambda/cc.
>> I believe a more complicated example would help greatly
> I agree. Once I check in the changes I'm working on now, I'll focus on
> a real world application designed to (hopefully) make money. I will
> likely open source it so that people can use it as a real world
> example of weblocks usage.
That would help immensely. An example is much more valuable if it is
maintained and represents the generally accepted way of doing things.
--J.
> Clicking on "Link" results in an empty template page, the dataform
> widget does not get rendered. However, if I replace my-other-page with
> this: ... things work just fine. I can't figure out why the dataform
> widget does not get rendered.
Consider what happens in your original snippet. When user clicks on
"Link", you call do-page with a function object #'my-other-page. What
happens, is that do-page places this function object into the root
composite. In this case my-other-page effectively becomes the root
widget. Then, when the user views the page, this widget is
"rendered". When the widget in question is a function (like in this
case), "rendering" involves simply calling it. So, my-other-page gets
called. It instantiates the dataform widget, but the dataform instance
never gets rendered. So, the user never sees anything.
You can modify my-other-page to say (render-widget
dataform-instance). This way the user will see the dataform, but keep in
mind that he'll see a new instances with every request. You'll have to
change my-other-page to an object that can maintain state in order to
not render a new instance on each request (either a closure that reuses
the instance, or a class with a slot, etc.)
Note, you don't have to call do-page (or yield, etc.) with a
function. So, instead of calling it with my-other-page, you can simply
pass an instance of a dataform widget. Then, the continuation will be
stored in the dataform instance. This way you can pass a function to
:on-success while instantiating a dataform, and call answer in that
function (just pass the dataform instance back to answer). This will
basically accomplish what you want as well.
Does this make sense?
> Is there a way to debug these kinds of problems?
Unfortunately the only way right now is to understand the code
better. I can't really think of another way (especially when it comes to
continuations, which are tricky). If you think of a way to improve
debugging, please let me know.
> Also, I don't think I understand fully when I need to use defun/cc and
> lambda/cc.
You use lambda/cc and defun/cc instead of the regular counterparts when
you use call/cc within, or you call any function that uses call/cc. This
means using do-place, do-page, do-confirmation, do-information,
do-choice, do-dialog, etc. Basically anything that initiates
continuation flow.
--
Regards,
Slava Akhmechet.
Ok, it's clear to me now. I was confused, for some reason I thought that
function widgets are supposed to return whatever should be rendered and
it will eventually get rendered later.
> You can modify my-other-page to say (render-widget
> dataform-instance). This way the user will see the dataform, but keep in
> mind that he'll see a new instances with every request. You'll have to
> change my-other-page to an object that can maintain state in order to
> not render a new instance on each request (either a closure that reuses
> the instance, or a class with a slot, etc.)
Right.
> Note, you don't have to call do-page (or yield, etc.) with a
> function. So, instead of calling it with my-other-page, you can simply
> pass an instance of a dataform widget. Then, the continuation will be
> stored in the dataform instance. This way you can pass a function to
> :on-success while instantiating a dataform, and call answer in that
> function (just pass the dataform instance back to answer). This will
> basically accomplish what you want as well.
Neat -- so the dataform instance can serve as the continuation parameter
to answer? I didn't know that.
Yes, it would help with my particular example, but I'm really trying to
learn how to build more complex, multi-step flows. And it isn't as easy
as I expected it to be.
> Does this make sense?
Yes, it makes perfect sense. Thanks for the excellent and clear
explanation.
>> Is there a way to debug these kinds of problems?
> Unfortunately the only way right now is to understand the code
> better. I can't really think of another way (especially when it comes to
> continuations, which are tricky). If you think of a way to improve
> debugging, please let me know.
The only thing that comes to mind is that in debug mode we might want to
check for common mistakes. In my example above, someone somewhere might
have noticed that my widget function didn't really render anything. As I
understand now, this is a pretty weird thing for a widget function to
do. A printed warning saying "Strange, widget function my-other-page
didn't render anything!" would have immediately pointed me in the right
direction.
But I do realize this isn't as easy as it sounds.
For me, the problem with understanding component-based web frameworks
was always the distinction between putting together components for your
pages and the actual act of rendering them. If you then add
continuation-based flows to the whole picture, things rapidly become
very tricky for beginners. It's basically three separate scaffoldings
one needs to keep track of.
I've just looked at the weblocks-demo example:
-- init-user-session doesn't render anything
-- initial-page does
-- but make-main-page does not
Now, I realize that if you look closely you should be able to notice
that initial-page is passed in as a function to yield, while
make-main-page gets evaluated and the result gets put in a list and
passed on to yield, which is very different -- I'm just saying those are
the things that beginners might stumble on.
>> Also, I don't think I understand fully when I need to use defun/cc and
>> lambda/cc.
> You use lambda/cc and defun/cc instead of the regular counterparts when
> you use call/cc within, or you call any function that uses call/cc. This
> means using do-place, do-page, do-confirmation, do-information,
> do-choice, do-dialog, etc. Basically anything that initiates
> continuation flow.
But not necessarily in anything that answers, right?
Also, I assume it is OK to have this:
(defun my-home-page ()
(with-html (:p (render-link (lambda/cc (&rest args)
(declare (ignorable args))
(do-page #'my-other-page))
"Link"))))
... e.g. just use lambda/cc for the callback -- the defun does not have
to be pushed through the transformer?
thanks,
--J.
> Neat -- so the dataform instance can serve as the continuation
> parameter to answer? I didn't know that.
Any widget can serve as the continuation parameter to answer. If you
take a look at base class for widget (WIDGET), it contains a
CONTINUATION slot where DO-PLACE stores the current continuation. You
can then pass the widget instance to ANSWER, which will clear the slot
and restore the continuation. Since WITH-FLOW/YIELD, as well as
DO-DIALOG, DO-PAGE, and all other DO-* variants use DO-PLACE, you can
always just pass a widget instance. Also, most callbacks (in DATAFORM
and others) must accept the widget instance as the first argument, so
you always have the instance of the widget when you need to pass it to
ANSWER.
> The only thing that comes to mind is that in debug mode we might want
> to check for common mistakes.
Hmm, ok. I see what you mean, but this definitely isn't on top of the
list in terms of priority.
> For me, the problem with understanding component-based web frameworks
> was always the distinction between putting together components for
> your pages and the actual act of rendering them. If you then add
> continuation-based flows to the whole picture, things rapidly become
> very tricky for beginners.
Somehow it was always very clear to me. You have a tree of widgets that
hold state and collections of other widgets. You manipulate the tree to
change your web application, and you call RENDER on the root to present
the whole thing (or appropriate parts). Continuations can be a bit
confusing, I agree.
> I've just looked at the weblocks-demo example:
> -- init-user-session doesn't render anything
> -- initial-page does
> -- but make-main-page does not
I see how this can be confusing. The thing is, I only included
functionality that treats function designators as widgets purely for
convinience. In many cases it's easier to just write a quick function
than to create a full blown class, and if the state you have to keep is
minimal you can use closures. The demo showcases this convinience
feature. In fact, using functions with continuations (and passing K to
them so they can pass it to ANSWER later) is a special case. Initially
these were meant to work only with widget instances. So yeah, I can see
how this can be very confusing. I'll try to clean up the demo to make
this more clear.
> But not necessarily in anything that answers, right?
Correct, ANSWER restores the continuation and does not require the
transformation, so you can define functions as you normally do.
> (defun my-home-page ()
> (with-html (:p (render-link (lambda/cc (&rest args)
> (declare (ignorable args))
> (do-page #'my-other-page))
> "Link"))))
>
> ... e.g. just use lambda/cc for the callback -- the defun does not have
> to be pushed through the transformer?
Yes, this is correct.
--
Regards,
Slava Akhmechet.