Friday, October 1, 2010

Lesson 7 - What? No Variables in Clojure?

If you noticed by now, we have gone through six lessons and haven't yet created a variable. At least not explicitly. We did create some lexically scoped variables that were created implicitly as function arguments, or bindings to the "loop" and "reduce" functions we saw. It is highly unlikely that you would go through six lessons in any programming language (non Lisp) and not create a variable.

There are global variables in Clojure, which you create with the "def" function. And the "let" function which allows you to create lexically scoped variables. However the fact that we haven't used them so far is a good thing. The more variables you have in your program, the more you have to keep track of them. And the more the chances of inadvertently introducing a bug.

Also in Clojure you can get away with not creating variables because of the usage of higher order functions. Higher order functions are functions that take other functions as arguments. You already saw the higher order function "reduce" in action.

Clojure variables are immutable. You cannot modify the values after creation. You can rebind a variable name to another value though. This is a feature of Clojure not a bug! There are good reasons for this, concurrency being one of them.

Let us get on with improving our web server.



(use 'clojure.contrib.server-socket)
(use '[clojure.string :only (join)])
(import  '(java.io BufferedReader InputStreamReader PrintWriter))
    
(def default-response
  { :status-line "HTTP/1.0 200 OK",
    :headers {:Content-Type "text/html"},
    :body "<h1>A Web Server written in Clojure</h1>"})
  
(defn send-response [response]
  (let [headers (assoc (response :headers)
                  :Content-Length (count (response :body)))]
    (println (response :status-line))
    (println
      (join
        (for [header (keys headers)]
          (format "%s: %s\n" (name header) (headers header)))))
    (print (response :body))))

(create-server
  8080
  (fn [in out]
    (binding

      [ *in* (BufferedReader. (InputStreamReader. in))
        *out* (PrintWriter. out)]
      (send-response default-response)
      (flush))))



Run the above code and browse to localhost:8080. What we have done here is set up a default response for our web server. We define a global variable default-response, which is a map structure with three keys and their corresponding values. Note that the value of the ":headers" key is itself another map.

We then define the send-response function which writes to the output stream, a given response map. The first thing the function does is define a lexically scoped variable called headers.


  (let [headers (assoc (response :headers)
                  :Content-Length (count (response :body)))]


The let function takes an array of bindings. Here there is only one binding. The variable "headers" is bound to the return value of the assoc function. The assoc function takes the :headers map of the response and adds a :Content-Length key, and its value, is returned by the count function which returns the length of the :body value of the response. The visibility of this bound "headers" variable is the body of the "let" function. Their are three "println" statements in this function. Let us look at the second one.

(join
  (for [header (keys headers)]
    (format "%s: %s\n" (name header) (headers header)))))

Let us look at the "for" statement first. It takes the collection of keys returned by (keys headers) and binds each value to the "header" variable and executes the "format" function for each header key. The format string takes two parameters, a string value of the header key (name header) and the value of the header (headers header) and replaces these two values in the format string. The collection of header lines returned by "for" is then joined by the "join" function.

In this case the output of the function will be

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 40


<h1>A Web Server written in Clojure</h1>


We tackle Macro's in the next Lesson.

3 comments:

  1. Hi,

    Thanks for your lessons so far! I'm a Clojure newbie ... I've found the following problem when running the last version of the server (running on Ubuntu 10.04, Sun JRE 1.6.0_21, clojure-1.2.0.jar, clojure-contrib-1.2.0.jar):

    Exception in thread "main" java.lang.Exception: Unable to resolve symbol: join in this context (server.clj:14)

    I tried adding the following at the top:

    (use 'clojure.set)

    but on each request from the browser, I get:

    Exception in thread "Thread-1" java.lang.RuntimeException: java.lang.IllegalArgumentException: Wrong number of args (1) passed to: set$join

    Any suggestions?

    Thanks!

    ReplyDelete
  2. Hi, I found a workaround to the issue with the server on this lesson, I'm not using join but str-join in send-response and it worked, so instead of

    (join
    (for [header (keys headers)]
    ...

    I'm using

    (str-join ""
    (for [header (keys headers)]
    ...

    This requires using str-utils, like this:

    (use 'clojure.contrib.str-utils)

    Anyway, thanks again for the excellent lessons on the blog.

    Regards,

    Denis

    ReplyDelete
  3. Somehow the code was missing the second line which should have actually read
    (use '[clojure.string :only (join)])
    If fixed that in the code now.
    You need not use str-join.
    Thanks for pointing out.

    ReplyDelete