hello world
The magic incantations to get things started:
(asdf :wispylisp) (in-package :wisp-mvc) ;; start aserve (start :port 2002) ;; easier aserve debug (net.aserve::debug-on :notrap) ;; html,css, and javascript depend on :invert (setf (readtable-case *readtable*) :invert) |
And the curse of tradition:
(defwethod hello () (render)) (defview hello () (:html (:body "Hello World."))) (defurlmap hello-map ("hello" :wethod hello)) ;; install the urlmap `hello-map' with root url-prefix "root", ;; so every path in hello-map is accessible via /root/<path> (attach-urlmap "root" 'hello-map) |
render
is intuitively like call-next-method
. It calls
the view rendering for the wethod with the same arguments provided
to the wethod itself. The view for hello
responds to the
request with html string.
A urlmap map is a set of url-prefixes to functions. In this simple case,
the wethod hello
is mapped to the prefix "hello", and the
urlmap hello-urlmap
itself is mapped to "root", yielding the
overall path "/root/hello" to access the wethod hello
.
Now, http://localhost:2002/root/hello
defwethod
defwethod
is like defmethod, it allows specialization of its
arguments. Let's define some foos:
(defwethod foo (a b) (render-html (:html (list a b)))) (defwethod foo ((a number) b) (render-html (:html `((number ,a) (t ,b))))) (defurlmap hello-map ("hello" :wethod hello) ("foo" :wethod foo)) |
Each of these foo
's is accessible through a unique url-pattern.
We can see the current active url-patterns by calling
get-effective-urls
:
(get-effective-urls 'hello-map) => ((:wethod (:url "/hello" "^/hello") :dispatch #<standard-generic-function hello> :discriminators (nil) :wethod-urls (("" "^$"))) (:wethod (:url "/foo" "^/foo"):dispatch #<standard-generic-function foo> :discriminators ((number t) (t string) (t t)) :wethod-urls (("/number_:arg1/:arg2" "^/number_([^/]+)/([^/]+)$") ("/:arg1/string_:arg2" "^/([^/]+)/string_([^/]+)$") ("/:arg1/:arg2" "^/([^/]+)/([^/]+)$")))) |
Between each pairs of "/" is one url-argument, with the very first "_" used to separate the type-specifier from the value of the url-argument.
First, see how the generic function hello
is associated with
the prefix "/hello", but because it doesn't take any arguments, this
prefix uniquely identifies hello
. The url-pattern for
foo
is more complicated. Its generic function is associated
with the prefix "/foo", and the rest of the request url is used to
determine with what arguments of what types to apply to foo.
"/number_:arg1/:arg2" says, the first argument is a number, and the
second argument has unspecified type. The generic function
^wisp-arg
is used to translate a url-argument into its proper
type before applying it to foo
. In the case of a number,
parse-integer
is used. In the case of an unspecified type, the
url-arg is passed in as a string. ^wisp-arg
is easily
extended to convert an object-id (id as a string) into an object of
some type stored in the elephant object database.
Let's try some foo. Before that, we've modified hello-map
, so
we need to reattach it before it comes into effect. This is
unfortunate.
(attach-urlmap "root" 'hello-map) |
http://localhost:2002/root/foo/arg1/arg2
http://localhost:2002/root/foo/number_314159/arg2
deform
deform
deforms an html form so that read-form
can be
used within a wethod to prompt for user input. When the form returns,
the wethod continues from the continuation.
Let's try making a simple stack calculator.
(deform stack-calculator (stack transcript) ;; The fields of the form. (+ - * / push new-number) ;; When user submits the form, the following expression is evaluated ;; and the value(s) returned to the caller's continuation. (let ((op (^symbol (find-if-not #'null (list + - * / push))))) (if push (list op (cons (parse-integer new-number) stack)) (list op stack))) ;; outputs the html for the form. (:input :name new-number) (:input :name push :type 'submit :value 'push) (:br) (:input :name + :type 'submit :value '+) (:input :name - :type 'submit :value "-") (:input :name * :type 'submit :value '*) (:input :name / :type 'submit :value '/) (:div :id 'stack :style (:border 1px solid $FF0000 :width 200px) (dolist (number stack) (html number (:br)))) (:div :id 'transcript :style (:border 1px solid $00FF00 :width 200px) (dolist (cmd transcript) (html cmd (:br))))) (defwethod calculator-loop () (loop with transcript for (op stack) = ;; `read-form' will output the html for the form, save the continuation, ;; and return immediately from `calculator-loop'. When the user submits the ;; form, the url-handler calls the continuation. ;; ;; To the programmer, it appears as though read-form returns (list op stack). (read-form 'stack-calculator nil nil) then (read-form 'stack-calculator stack transcript) do (if (eql op 'push) (push (list 'push (car stack)) transcript) (let ((result (funcall (symbol-function op) (second stack) (first stack)))) (push (format nil "~S => ~S" (list op (second stack) (first stack)) result) transcript) (setf stack (cons result (nthcdr 2 stack))))))) (defurlmap calculator ;; maps the url /calc to the wethod calculator-loop ("" :wethod calculator-loop)) (attach-urlmap "calc" 'calculator) |
For this to work, we need the following voodoo:
(defurlmap wisp-sys ;; call-from-k takes the string in place of `:k-id' as its (single) argument. ("form-k/:k-id" :handler call-form-k)) (attach-urlmap "wisp-sys" 'wisp-sys) |
The biggest benefit of the use of continuation based forms is that the program "remembers" the state of a computation, without the programmer having to explicitly do so. The main drawbacks are:
Continuation, in the context of web-programming, is mainly used to deal with the problem of window duplication and back button in an interactive web interface. AJAX tackles the problem in a different way, by using an event-driven model, thus never really leaving the page. Continuations are useful when the designer in the interest of accessibility has to forego AJAX. Another desirable property of continuation-based applications is that program states are represented by bookmarkable URLs.
It is interesting to note that the DOJO javascript toolkit has support for back button and bookmarkable AJAX event. This essentially reintroduces all the problems implied by the statelessness of the web-protocol. So again, continuation can be useful in this context.
How Deform Works
(deform stack-calculator (stack transcript) (+ - * / push new-number) (let ((op (^symbol (find-if-not #'null (list + - * / push))))) (if push (list op (cons (parse-integer new-number) stack)) (list op stack))) ... ) |
The form stack-calculator
is parameterizable by two arguments,
stack and transcript. This allows calculator-loop
to
communicate to stack-calculator
by passing in the current
stack, and the transcript for input history. Like this:
(read-form 'stack-calculator stack transcript) |
The form allows 6 inputs, (+ - * / push new-number). When the user submits the form, these symbols are bound to the string values of the corresponding input fields. The form:
(let ((op (^symbol (find-if-not #'null (list + - * / push))))) (if push (list op (cons (parse-integer new-number) stack)) (list op stack))) |
has access to (+ - * / push new-number). This form is evaluated, and the value passed to the suspended continuation:
for (op stack) = (read-form 'stack-calculator nil nil) then (read-form 'stack-calculator stack transcript) do ... |
so (op stack) in calculator-loop
is destructurally bound to the
value returned by stack-calculator
. The loop body continues
with new values of op and stack, updating the transcript to reflect
the newly received input, and finally looping back to the beginning
and call stack-calculator
again to prompt for new input. Note
that
(read-form 'stack-calculator stack transcript) |
now uses the new modified value for transcript to call stack-calculator
.
Fancy Layout with DOJO
DOJO makes your life easier at the cost of having non-comformant HTML. Oh well.
(defwethod test-dojo () (render-view 'layout)) (defview layout () (:html (:header (:js (= djConfig (object isDebug false))) (:js-src /src/dojo/dojo.js) (:js-require dojo.widget.*) (:css ((:and html body) :margin 0 :padding 0 :overflow hidden :width 100% :height 100%))) (:body (:layout-div :child-priority "none" :style (:width 80% :height 300px) (:top :style (:background red) "top") (:bottom :style (:background $000000 :color $ffffff) "bottom") (:left :style (:background $444444 :color $ffffff) "left") (:right :style (:background $888888)"right") (:top :style (:background $cccccc)"top 2") (:right :style (:background green :color $ffffff) "right") (:bottom :style (:background blue)"bottom 2") (:left :style (:background yellow :color black) "left") (:client :style (:text-align center)"How about 42?"))))) (defurlmap test-dojo ("test" :wethod test-dojo)) (attach-urlmap "dojo" 'test-dojo) |
Now try, http://localhost:2002/dojo/test
deftag
:layout-div is the dojo layout container widget. It is a macro defined
with deftag
. deftag
allows lambda-list of the form:
(<required-arg>* [[&key <key-arg>*]] [[&other-keys <symbol>]] [[&rest <symbol>]]) |
This lambda-list differs from the original lambda-list in that:
This is called the rkr-lambda-list
, where rkr stands for
"required-key-rest". It allows some required arguments, an arbitrary
number of keyword argument pairs, and the first non-keyword value and
whatever follows in the argument list are collected into &rest. It
makes defining new html tag very easy. Let's see how:layout-div
is implemented:
(deftag :layout-div (&key child-priority &other-keys others &rest body) (flet ((make-pane (child) (let ((align (car child))) (if (find align '(:top :bottom :left :right :client :flood)) `(:pane :layout-align ,(^string align) ,@(cdr child)) child)))) `(:div :dojoType "LayoutContainer" ,@(when child-priority `(:layout-child-priority ,child-priority)) ,@others ,@(mapcar #'make-pane body)))) |
It's used like this:
(:layout-div :child-priority "none" :style (:width 80% :height 300px) (:top :style (:background red) "top") (:bottom :style (:background $000000 :color $ffffff) "bottom") ...) |
Note 3 things:
:layout-div
but is captured by
&other-keys others. It is spliced into the :div that actually
implements :layout-div
. Like so:
`(:div :dojoType "LayoutContainer" ... ,@others ...) |
Inconclusion
This tutorial demostrates some functionalities of Wispy Lisp. Although Wispy Lisp can generate arbitrary javascript and css, their integration with the rest of the framework is rather halfbaked. The use of elephant for object persistence has great promises, and is worth investigating further.
In short, Wispy Lisp is very much a student project.
Ack
This marks the end of SoC2006, and the beginning of Wispy Lisp. I am grateful for this great opportunity afforded to me by Google and LispNYC.
Thanks LispVAN its love for lisp, when the world raves on about Java, C++, PHP, and other monstrocities.
Thank you, Marco, for helping me along the way, and allowing me the greatest freedom possible. Best wishes.
This document was generated by howard on August, 24 2006 using texi2html 1.76.