Beating the Arc challenge - In Clojure

2009-12-20 12:00:52

A while ago Paul Graham released his own Lisp implementation called Arc. It was not exact well received, so Paul Graham responded to some of the criticism on his blog and even proposed a challenge, which served to demonstrate the power of Arc - Challenge accepted.




Preface

You can read Mr. Grahams initial response to the criticism before proceeding, it will give you a better idea of what he wants this challenge to demonstrate.

The Challenge is this:

Write a program which serves websites

Paul Grahams solution is very concise and to the point:

(defop said req
  (aform [w/link (pr "you said: " (arg _ "foo"))
           (pr "click here")]
    (input "foo")
    (submit)))


I haven't run that, but something tells me that what you see cant be the whole story. I see that 'said' is being defined but now called and no server started - where's the rest? Is that really all?

Responses

Close following each other this challenge was 'beat' in both Haskell and Go and the results posted on Hacker News. In Arc's forums there's a host of other solutions in many languages, but I didn't see a Clojure version, so here we go.

The first page needs to respond to a GET request and show a simple HTML form:

  (GET "/sayit/"  (html
                   (form-to [:post "/click/"]
                     (text-field "saying" "")
                     (submit-button "Say it"))))

As you can imagine, that renders a form (form-to) which contains a text-field and a submit button. If the terminology seems unfamiliar, its because you haven't yet played around with one of Clojures web-frameworks: Compojure. The results is:

When 'Say it' is clicked it posts to "/click/", which we then can define:

  (POST "/click/" (html
                   (link-to (str "/click/" (:saying params))
                            "Click here"))))

Mr. Graham didn't specify how the data should be carried over and in Clojure I have a host of ways to do it, since my code is not just being run while the page is rendered I can share memory between requests. To keep things simple however, I opted to just pass the parameter (:saying params) directly in the URL:

Update: As the now famous Alex Osbourne points out, Mr. Graham explicitly says: Dont carry the message in the URL. That gives me an oportunity to demonstrate Compojures great Session middle-ware:

  (POST "/click/" [(session-assoc :message (:saying params))
                   (html (link-to "/click/" "Click here"))]))

Compared to above there is just 1 addition, namely the call to session-assoc. That modifies the session for our current user, each user having his own Session ID generated by Jetty. It sets the :message key, to whatever he typed in the input field.

So now there's only the last page left, which needs to read the parameter from the URL and output the result:

  (GET "/click/*" (html "You said: " (:* params)))

The * I placed in the route makes Compojure put whatever comes after the URL in the params hash-map under the key *, so this is really no more than string concatenation. As always, I call Compojures html function to convert the output to sane html. So the final page now shows:

Arc-challenge solved in our usual Lispy style. For the program to run fully stand-alone we need to add a dependency, bundle the routes and boot the server. The entire program weighs in at 15 lines:

(use 'compojure)

(defroutes arc-challenge
  (GET "/favicon.ico" nil)
  (GET "/sayit/"  (html
                   (form-to [:post "/click/"]
                     (text-field "saying" "")
                     (submit-button "Say it"))))
  (GET "/click/"  (html (str "You said: " (:message session))))
  (POST "/click/" [(session-assoc :message (:saying params))
                   (html (link-to "/click/" "Click here"))]))

(run-server {:port 8080} "/*"
            (servlet
             (with-session arc-challenge)))

Update: I added a (with-session ..) in the call to run-server, to enable Compojures Session middleware and also I get the users input from the new 'session' variable, instead of 'params' which I used above - Now we comply with Mr. Grahams request.


Conclusion

One of the things I really like about Clojure, but the same goes for Lisp in general, is this ability to write DSLs so easily and elegantly. In its short life Clojure has already seen many DSLs spring up and more are inbound. When you can easily make DSLs you tendency is that you make more of them, which means that whatever ability you have in Clojure quickly becomes applicable for HTML, SQL, Graphics, Statistics, you name it - You never have to leave your primary domain.

Hope you enjoyed the read.


Benoît Huron
2009-12-20 14:10:01
From "Take the Arc challenge" :

"The third page must only show what the user actually typed. I.e. the value entered in the input field must not be passed in the url, or it would be possible to change the behavior of the final page by editing the url."
Lau
2009-12-20 14:11:39
@Benolt: Hey, thanks for stopping by.

Look for the 2 "Update" sections, where you can see I added Session handling. The server automatically assigns a unique session :id to each visitor, which the session middleware taps into.
WalterGR
2009-12-21 01:39:20
I like the color scheme you use for your code.  Is there any way you could share it?

Also, how do you generate the colorized HTML?

Thanks!

(Sorry if this is a double post - my previous attempt didn't seem to take.)
Lau
2009-12-21 09:23:07
@Walter: Hey :)

The color-theme is charcoal-black, loaded after sitaramv-nt. The load-order makes a difference, but not one that you can tell from the snippets.

For moving code unto the blog, I simply mark the region I need and hit M-x blog, which then puts the htmlized version into the clip-board, ready for pasting. The blog-function is simply a macro I recorded and output to Lisp code. For those new to elisp it goes like this 'C-x (' to begin recording the macro. Then while recording run M-x htmlize, C-x o to switch into the new buffer, mark the entire file and C-w to cut, finally use 'C-x )' to stop recording. To name this macro use C-x C-k n. Then enter your .emacs config and hit M-x insert-kbd-macro to output the final Lisp code. Through complexity comes simplicity :)

Thanks for stopping by.
Andreas Krey
2009-12-24 14:04:57
I think one of the points of the original code is that there is no wiring ("/click/" url) between the submit button and the resultingly invoked URL; instead the action to be done when the link is clicked is specified as a function/lambda/code block, and what to use as URL to implement this is up to the framework.