[Bese-devel] ucw style, concepts and random info

Marco Baringer mb at bese.it
Tue Dec 7 09:42:49 UTC 2004


Antonio Menezes Leitao and I had an e-mail exchange in which i ended
up writing a lot of stuff about the ucw "mindset" (how things should
be looked at) and a couple of details which aren't easily gleanable
from the docs.

he's attempting to do something very cool and i figured his questions
(and my answers) might be of interest to others.

========================================================================
From: "Marco Baringer" <mb at bese.it>
Subject: Re: UnCommonWeb/SBCL/x86
To: Antonio Menezes Leitao <Antonio.Leitao at evaluator.pt>
Date: Sat, 04 Dec 2004 15:30:56 +0100

Antonio Menezes Leitao <Antonio.Leitao at evaluator.pt> writes:

> The idea is to create a repl that reads one expression at a time,
> evaluates it and prints the result...just like your example in the
> admin application...but where you can also correctly evaluate an
> expression such as
>
> (+ 1 (read))
>
> In this case, during the evaluation of the expression, the client will
> get a new page that reads a value and uses it during the computation.
>
> Even more interesting is the situation
>
> (+ (read) (read))
>
> Here, the full power of continuations will be used as it is necessary
> to read a first value, then a second one and, finally, to present the
> result to the client.  Obviously, we want to be able to go back to
> either of the reads and proceed with different values.
>
> Here is my solution:
> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
> ;;The application
> (defapplication eval-application 
>   (:url-prefix "/ucw/eval/"))
>
> (defentry-point "index.ucw" (:application eval-application) ()
>   (call 'eval-app))
>
> ;;The main window
> (defclass eval-app (simple-window-component)
>   ((body :initarg :body :accessor eval-app-body 
>          :initform nil :component repl))
>   (:metaclass standard-component-class)
>   (:default-initargs :title "UCW Evaluator"))

[this sequence of forms really deserves a macro or something...]

what do you think about adding an :entry-point initarg to the
standard-component-class class?

> (defmethod render-on ((res response) (app eval-app))
>   (<:html
>    (<:head
>     (<:title "UCW Evaluator."))
>    (<:body
>     (<:h1 "UCW Evaluator.")
>     (<:hr)
>     (render-on res (eval-app-body app))
>     (<:hr)
>     (<:A :href "index.ucw" "Start again."))))

since eval-app is a simple-window-component you don't need (and
really shouldn't) output <:html and <:head on its render-on
method. something like this is more appropiate:

(defmethod render-on ((res response) (app eval-app))
  (<:h1 "UCW Evaluator")
  (<:hr)
  (render-on res (eval-app-body app))
  (<:hr)
  (<:a :href "index.ucw" "Start again."))

thanks to simple-window-component's render-on :wrapping method this
will actually get called "within" a <body> tag and the <title> will
get set thanks to the :title initarg you passed.

> ;;The read-eval-print component
> (defclass repl (component)
>   ((form :accessor repl-form :initarg :form :initform nil)
>    (value :accessor repl-value :initarg :value :initform ""))
>   (:metaclass standard-component-class))
>
> (defmethod render-on ((res response) (app repl))
>   (<:div
>    (<:h2 "REPL")
>    (<ucw:form :action (evaluate-form app)
>     (<:p "The form: "
> 	 (<ucw:input :accessor (repl-form app))
> 	 " "
> 	 (<:input :type "submit" :value "Evaluates to")
> 	 " "
> 	 (<:as-is (repl-value app))))))

this is exactly what i'd do.

> ;;The action that is executed when we ask for the evaluation of an
> ;;expression.  Unfortunately, we cannot use eval because it doesn't
> ;;mix with the CPS framework so we build a new one named eval/cc.
> ;;Note that it is also necessary to pass the self argument.
> (defaction evaluate-form ((app repl))
>   (let ((expr (read-from-string (repl-form app))))
>     (setf (repl-value app)
> 	  (princ-to-string
> 	   (eval/cc self expr)))))

minor comment: i'd set (repl-value app) to the actual value returned
by eval/cc and then convert it to a string in the render-on method
(just so that you can inspect and reuse returned values).

> ;;Here is our (extremely simple) eval.
> (defun/cc eval/cc (self expr)
>   (cond ((numberp expr) expr)
> 	((atom expr) "Can't eval other atoms")
> 	(t (case (first expr)
> 	     ((read) (call 'reader))  ;;special treatment of the read function
> 	     (t (apply (first expr)
> 		       (evlist/cc self (rest expr))))))))
>
> (defun/cc evlist/cc (self exprs)
>   (if (endp exprs)
>     (list)
>     (cons (eval/cc self (first exprs))
> 	  (evlist/cc self (rest exprs)))))

this is the right idea, but will unfortunely fail with something like:

(eval/cc self '(if (whatever) (read) (something-else)))

since if isn't a function you can't apply it. you don't really want to
have to write a code-walker, and ucw already has one, so you can (if
you know about ucw internals) use the underlying machinery.

before i write the code let me explain what i want to accomplish: we
have a form (the result of (read-from-string (repl-form app))) which
we want to eval. we also want this form to be able to use the normal
call/answer machinery (hidden under the uses of read). in other words
we want to evaluate an action whose body is only know at runtime,
we'll use eval, but we need to put much of the action body, and some
internal ucw machinery, in the eval:

(defaction evaluate-form ((app repl))
  (let ((expr (read-from-string (repl-form app))))
    (eval
     `(with-call/cc
        (let ((self ,app))
          (macrolet ((read () '(call 'reader)))
            (setf (repl-value ,app) (princ-to-string ,expr))))))))

let me break that apart:

- we want the code in expr te be able to make calls to CALL, in order
  for that to work we need 2 things: the with-call/cc which will
  setup the proper continuations and the binding of app to self so
  that call-component gets the right calling-component argument.

- we want the read "function" to actually call a component, instead
  of definig a new function (via defun/cc) we just setup a macrolet
  whcih transforms (read) into (CALL 'READER).

- finally we do whatever the user inputed and set the value slot of
  app.

NB: the setf form _must_ be within the with-call/cc form. this is
because, deep down underneath ucw, the body of with-call/cc forms
always return, if we did:

(setf (repl-value app) (eval (with-call/cc ...)))

then we'd have repl-value set to various components (when read was
actually called) and when the reader component finally returned they
wouldn't actually change the value slot. this is because the
continuation created by with-call/cc can only extend as far as the
with-call/cc form, since setf is outside of the with-call/cc we never
manage to return from the eval.

does that make any sense?

> ;;Finally, the reader component.
> (defclass reader (component)
>   ((form :accessor reader-form :initarg :form :initform nil))
>   (:metaclass standard-component-class))
>
> (defmethod render-on ((res response) (app reader))
>   (<:div
>    (<:h2 "READING A VALUE")
>    (<ucw:form
>     :action (read-it app)
>     (<ucw:input :accessor (reader-form app))
>     " "
>     (<:input :type "submit" :value "Take that!"))))
>
> ;;After reading an expression, just return it to the caller
> (defaction read-it ((app reader))
>   (answer (read-from-string (reader-form app))))
> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
>
> If you still have patience to read, I would like to hear your comments
> on the following issues:
>
> - Is my application correctly done?  Should I use something else?

i think, other than the minor misunderstanding about
simple-window-component (due a lack of documentation on my part)
that's exactly what i'd do.

> - It seems to me that we always need to invoke the call and answer
>   operations from within actions...unless we take special measures
>   (see below).  The good thing is that we get the proper behaviour for
>   the 'back' button.

yes, call and answer MUST be used within a defaction form. this is
due to:

1) call and answer expand into uses of the defmethod/cc methods
   call-component and answer-component which require the presence af a
   lexical variable named SELF.

2) call and answer must be made within the lexical scope of
   with-call/cc if we want the continuation parameter to be passed
   automagically.

you could directly use call-component and answer-component (even
outside of actions) but you will have to manully set the continuation
and the calling-component it the call'd component.

i've occasianly used answer-component outside of defaction (mainly in
render-on methods to not have to create a simple action which just
does answer-component), though now i generally use the OK action.

> - However, I'm not so concerned about that (admitidly nice) behavior
>   as I am regarding the code style I want to use (which is the usual
>   direct style that will be translated to CPS. I really want to be
>   able to invoke a "function" that demands more data from the client
>   without disturbing the application (even if it needs to present
>   another page, collect results and return them to the point that
>   "called" it.
>
> - This direct style entails that I want to be able to define all sorts
>   of functions and, somewhere in the middle of all those functions,
>   call and answer operations will be used.  I presume that, in this
>   case, we need to define all those functions in CPS (that is, using
>   defun/cc and such) and, moreover, we need to pass the extra argument
>   self in order for call to work.  That's the reason why I needed to
>   define my own pseudo-eval and a special read function.  Is there a
>   workaround?  In particular, it doesn't seem logical to define a
>   function with an extra parameter 'self' that is never used except in
>   some invisible transformation of a (call ...) form.

unfortunetly you're hitting a hard limitiation. since ucw fakes
continuations, and does not actually examine the stack, you have to
put call/answer directly in the defaction (or defentry-point) and
can't put calls to functions without using defun/cc (or defaction or
defmethod/cc). this also requires that the defun/cc function adhere to
the cps transformer's limitations (no unwind-protect around call/cc,
etc.)

what could be done is make self a dynamic variable, and the current
binding would be captured by call-component, this would aliviate the
need to pass a slef argument, but wouldn't remove the fundamental
limitation.

now, let's say you have an application which does a lot of "stuff,"
some of this "stuff" requires input from the user. what i'd do is
split up the "stuff" into small enough pieces so that each user
operation is handled by a single component and then tie the componets
together with actions (as opposed to defun/cc). if you have a complex
operation which requires a lot of input i'd do something like this:

(defaction complex-operation ((app application-window))
  (call 'show-result
        :result (do-complex-op (call 'get-arg1)
                               (call 'get-arg2)
                               ...))

now the (relativly) simple get-arg components can do whatever they
want, once we've gotten all the input we then pass the values to a
regular lisp function.

if we want the user to have a menu of the options for do-complex-op
and we don't want to require them to set all the values (and we
provide defaults), i'd do:

(defcomponent complex-operation (container)
  ((arg1 :component get-arg1-with-defaults)
   (arg2 :component get-arg2-with-defaults)
   ...
   (result :accessor result :component 'show-result))
  (:metaclass standard-component-class))

(defaction do-complex-op ((op complex-operation))
  (setf (value (result op))
        (do-it (value (arg1 op))
               (value (arg2 op)) ...)))

(defmethod render-on ((res response) (op complex-operation))
  (render-argument-menu)
  (render-do-complex-op-button)
  (if (have-result (result op))
      (render-on res (result op))
      (<:p "Output not yet available.")))

basically i'd have a component which allowed the user to set the
input for the operation and, when there's some output, shows the
output with the supplied args. i'd have a component for every
parameter which knows how to transform the user's input into a lisp
value suitable for passing to the do-it function. i'd also setup the
result componet so that it knows when its ready to be rendered and
when it isn't.

you could just as well not have result be a slot of the componet put
call a show-result component from the do-complex-op action, whatever.

> Thanks a lot for your time and, once again, congratulations for your
> excelent framework.  I'm looking forward to develop more interesting
> applications.

here's your example with my edits:

------------------------------------------------------------------------
(in-package :it.bese.ucw-user)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;The application
(defapplication eval-application 
  (:url-prefix "/ucw/eval/"))

(defentry-point "index.ucw" (:application eval-application) ()
  (call 'eval-app-window))

;;The main window
(defclass eval-app-window (simple-window-component)
  ((body :initarg :body :accessor eval-app-body 
         :initform nil :component repl))
  (:metaclass standard-component-class)
  (:default-initargs :title "UCW Evaluator"))

(defmethod render-on ((res response) (app eval-app-window))
  (<:h1 "UCW Evaluator.")
  (<:hr)
  (render-on res (eval-app-body app))
  (<:hr)
  (<:A :href "index.ucw" "Start again."))

;;The read-eval-print component
(defclass repl (component)
  ((form :accessor repl-form :initarg :form :initform nil)
   (value :accessor repl-value :initarg :value :initform ""))
  (:metaclass standard-component-class))

(defmethod render-on ((res response) (app repl))
  (<:div
   (<:h2 "REPL")
   (<ucw:form :action (evaluate-form app)
    (<:p "The form: "
	 (<ucw:input :accessor (repl-form app))
	 " "
	 (<:input :type "submit" :value "Evaluates to")
	 " "
	 (<:as-is (repl-value app))))))

(defaction evaluate-form ((app repl))
  (let ((expr (read-from-string (repl-form app))))
    (eval
     `(with-call/cc
        (let ((self ,app))
          (macrolet ((read () '(call 'reader)))
            (setf (repl-value ,app) (princ-to-string ,expr))))))))

;;Finally, the reader component.
(defclass reader (component)
  ((form :accessor reader-form :initarg :form :initform nil))
  (:metaclass standard-component-class))

(defmethod render-on ((res response) (app reader))
  (<:div
   (<:h2 "READING A VALUE")
   (<ucw:form :action (read-it app)
    (<ucw:input :accessor (reader-form app))
    " "
    (<:input :type "submit" :value "Take that!"))))

;;After reading an expression, just return it to the caller
(defaction read-it ((app reader))
  (answer (read-from-string (reader-form app))))
------------------------------------------------------------------------

hope this helps.
========================================================================

-- 
-Marco
Ring the bells that still can ring.
Forget your perfect offering.
There is a crack in everything.
That's how the light gets in.
     -Leonard Cohen



More information about the bese-devel mailing list