Intro to Clojure on the Web
Lisp is one of those languages that people either love or hate. Count me among the Lisp lovers. I was brainwashed during my undergraduate studies at MIT to believe that Lisp is the only "real" programming language out there, and that anything else is a pale imitation. True, I use Python and Ruby in my day-to-day work, but I often wish I had the chance to work with Lisp on a regular basis.
One window of opportunity to do exactly that has opened in the past few years. Clojure, a modern variant of Lisp that runs on the Java virtual machine (JVM), has been taking the programming world by storm. It's a real Lisp, which means that it has all of the goodness you would want and expect: functional programming paradigms, easy use of complex data structures and even such advanced facilities as macros. Unlike other Lisps, and contributing in no small part to its success, Clojure sits on top of the JVM, meaning that it can interoperate with Java objects and also work in many existing environments.
In this article, I want to share some of my experiences with starting to experiment with Clojure for Web development. Although I don't foresee using Clojure in much of my professional work, I do believe it's useful and important always to be trying new languages, frameworks and paradigms. Clojure combines Lisp and the JVM in just the right quantities to make it somewhat mainstream, which makes it more interesting than just a cool language that no one is really using for anything practical.
Clojure BasicsClojure, as I mentioned above, is a version of Lisp that's based on the JVM. This means if you're going to run Clojure programs, you're also going to need a copy of Java. Fortunately, that's not much of an issue nowadays, given Java's popularity. Clojure itself comes as a Java archive (JAR) file, which you then can execute.
But, given the number of Clojure packages and libraries you'll likely want to use, you would be better off using Leiningen, a package manager for installing Clojure and Clojure-related packages. (The name is from a story, "Leiningen and the Ants", and is an indication of how the Clojure community doesn't want to use the established dependency-management system, Ant.) You definitely will want to install Leiningen. If your Linux distribution doesn't include a modern copy already, you can download the shell script from https://raw.github.com/technomancy/leiningen/stable/bin/lein.
Execute this shell script, putting it in your PATH. After you download the Leiningen jarfile, it will download and install Leiningen in your ~/.lein directory (also known as LEIN_HOME). That's all you need in order to start creating a Clojure Web application.
With Leiningen installed, you can create a Web application. But in
order to do that, you'll need to decide which framework to use.
Typically, you create a new Clojure project with lein new
, either
naming the project on which you want to work (lein new
myproject
),
or by naming the template you wish to copy and then the name of the
project (lein new mytemplate myproject
). You can get a list of
existing templates by executing lein help new
or by looking at the
https://clojars.org site, a repository for Clojure jarfiles and libraries.
You also can open an REPL (read-eval-print loop) in order to communicate directly with Clojure. I'm not going to go into all the details here, but Clojure supports all the basic data types you would expect, some of which are mapped to Java classes. Clojure supports integers and strings, lists and vectors, maps (that is, dictionaries or hashes) and sets. And like all Lisps, Clojure indicates that you want to evaluate (that is, run) code by putting it inside parentheses and putting the function name first. Thus, you can say:
(println "Hello")
(println (str "Hello," " " "Reuven"))
(println (str (+ 3 5)))
You also can assign variables in Clojure. One of the important things to know about Clojure is that all data is immutable. This is somewhat familiar territory for Python programmers, who are used to having some immutable data types (for example, strings and tuples) in the language. In Clojure, all data is immutable, which means that in order to "change" a string, list, vector or any other data type, you really must reassign the same variable to a new piece of data. For example:
user=> (def person "Reuven")
#'user/person
user=> (def person (str person " Lerner"))
#'user/person
user=> person
"Reuven Lerner"
Although it might seem strange to have all data be immutable, this tends to reduce or remove a large number of concurrency problems. It also is surprisingly natural to work with given the number of functions in Clojure that transform existing data and the ability to use "def" to define things.
You also can create maps, which are Clojure's implementation of hashes or dictionaries:
user=> (def m {:a 1 :b 2})
#'user/m
user=> (get m :a)
1
user=> (get m :x)
nil
You can get the value associated with the key "x", or a default if you prefer not to get nil back:
user=> (get m :x "None")
"None"
Remember, you can't change your map, because data in Clojure is immutable. However, you can add it to another map, the values of which will override yours:
user=> (assoc m :a 100)
{:a 100, :b 2}
One final thing I should point out before diving in is that you
can (of course) create functions in Clojure. You can create an
anonymous function with fn
:
(fn [first second] (+ first second))
The above defines a new function that takes two parameters and adds
them together. However, it's usually nice to put these into a named
function, which you can do with def
:
user=> (def add (fn [first second] (+ first second)))
#'user/add
user=> (add 5 3)
8
Because this is common, you also can use the defn
macro, which
combines def
and fn
together:
user=> (add 5 3)
8
user=> (defn add [first second] (+ first second))
#'user/add
user=> (add 5 3)
8
Web Development
Now that you have seen the basics of Clojure, let's consider what is involved in Web development. You need to have an HTTP server that will accept requests, as well as a program (typically known as a "router") that decides which functions will be executed for each requested URL. Then, you want to return data to the user, typically in the form of HTML files.
Now, various Web application frameworks approach this problem differently. In the most primitive ones (for example, CGI programs), the "application" really is invoked only on a single program. Large frameworks, such as Ruby on Rails, handle all of these parts and even allow you to swap parts out—and each of those parts is done using instances of different classes that handle the appropriate information.
Now, although Clojure is built on top of the JVM and uses some of the primitive Java classes as its fundamental data types, it is not an object-oriented language. Clojure is a functional language, which means that all of the aforementioned steps will be handled using functions—either those that you define or those that have been defined for you by a Web framework.
For the purposes of this article, I'm going to use Compojure, a simple Web framework for Clojure. To create a basic Compojure project, you can use Leiningen:
lein new compojure cjtest
This potentially will download a number of libraries from clojars (the Clojure library repository), then the new Clojure project ("cjtest"). Once that is done, you can run it by going into the cjtest directory and executing:
lein ring server
This will download and install whatever dependencies exist, and it then will start a simple HTTP server, by default on port 3000. You then can visit this simple application at http://localhost:3000, where you'll be greeted by "Hello, world" from Compojure.
Now, how does this work? How does Compojure know what to do?
The answer is in the Clojure project file (project.clj). The default, generic Compojure project is defined using (of course!) Lisp code. Indeed, it's pretty standard in the Lisp world to use Lisp code as data. The default project created for me by Leiningen looks like this:
(defproject cjtest "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.5.1"]
[compojure "1.1.5"]]
:plugins [[lein-ring "0.8.5"]]
:ring {:handler cjtest.handler/app}
:profiles
{:dev {:dependencies [[ring-mock "0.1.5"]]}})
This uses Clojure's defproject
macro, giving it a name
"cjtest" and
a version number. Then there are several keywords, a Clojure data type
used as (among other things) the keys in maps. Thus, the above example shows
Clojure that the program has the default description, and it depends
on both Clojure and Compojure (and specifies the versions of each).
A second configuration file, also written in Lisp code, sits within the src/cjtest directory and is called handler.clj. The default looks like this:
(ns cjtest.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]))
(defroutes app-routes
(GET "/" [] "Hello World")
(route/resources "/")
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
In other words, if someone requests "/", it will return "Hello World". You then add a route for any static resources you might want to server, which should be in the resources directory of your Clojure project. You then add a not-found route for handling 404 errors.
You can run this amazing new Web application with:
lein ring server
If you're wondering what "ring" is doing there, suffice it to say that Ring is the Clojure library that handles just about all Web-related activity. It's a low-level library, however, and it doesn't function as an actual Web application framework. It uses the standard Jetty system for Java Web applications, although you don't need to worry about that at this stage, given the automation that Leiningen provides; any missing libraries or dependencies will be downloaded when you run Ring.
Sure enough, after running the above, if you go to /, you get a
"Hello world" message back from the server, because you had mapped
GET
/
to a string. If you want, you instead can map it to a function by
having square brackets following the route name, and then adding a
function body:
(defroutes app-routes
(GET "/" [] "Hello World")
(GET "/fancy/:name" [name]
(str "Hello, " name))
(route/resources "/")
(route/not-found "Not Found"))
You even can add HTML tags for some formatting:
(defroutes app-routes
(GET "/" [] "Hello World")
(GET "/fancy/:name" [name]
(str "<p><b>Hello</b>, " name "<p>"))
(route/resources "/")
(route/not-found "Not Found"))
Notice how in Lisp, because of the prefix syntax, you don't need to use
+ or any other operator to create a string from three (or any number)
parts. You just add additional parameters, separated by spaces, and
they are added to the new string that str
returns.
If you don't want to have your function inline, you always
can define it elsewhere and then pass it to defroutes
:
(defn say-hello
[req]
(str "<p><b>Hello</b>, "
↪(get (get req :route-params) :name) "<p>"))
(defroutes app-routes
(GET "/" [] "Hello World")
(GET "/fancy/:name" [name] say-hello)
(route/resources "/")
(route/not-found "Not Found"))
Notice how this function, say-hello
, takes a single argument (called
req
), a map of maps that contains all the data having to do with
the HTTP request you received. If you want to grab the
name
parameter
that was defined in the route, you have to get it from the
route-params
map inside the req
map
that your function is
passed. You probably will want to explore the req
object to
understand just what Compojure is passing and how to work with such
data.
Now, Compojure advertises itself as a routing mechanism for Web applications. This raises the question of what sorts of templates Compojure brings to the table. And, the answer is none. That's right, Compojure itself expects you to return strings containing HTML, but how you make those strings is up to you.
Thus, you can create strings, as shown above, in order to return a value. Or, you can use a templating engine, which in the Clojure world, simply means including another dependency and using the functions defined by that dependency:
(defproject cjtest "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.5.1"]
[compojure "1.1.5"]
[hiccup "1.0.3"]]
:plugins [[lein-ring "0.8.5"]]
:ring {:handler cjtest.handler/app}
:profiles
{:dev {:dependencies [[ring-mock "0.1.5"]]}})
Now, within the handler.clj file, you need to make another change, so that you can have access to the "hiccup" HTML-generation system:
(ns cjtest.handler
(:use compojure.core hiccup.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]))
With these in place, you now can create HTML by using a function and Clojure's vector syntax:
(defn say-hello
[req]
(html [:p [:b "Hello, "
↪(get (get req :route-params) :name) ]]))
Now, if you look at the parentheses and say, "Yikes, that's what I dislike about Lisp", I'll understand. But if you look at this as Lisp code and realize that this means your templates automatically are valid HTML if they are valid Lisp, you're thinking in the terms that Clojure wants to encourage—that is, using Lisp data structures as much as possible and feeding them to functions that know how to work with them.
ConclusionAs you can see, Clojure provides the simplicity of Lisp's structure and syntax while staying within the world of Java and the JVM. My next article will look a bit deeper at Compojure, investigating forms, database connectivity and other issues that modern Web applications want to use.
ResourcesThe home page for the Clojure language is http://clojure.org and includes a great deal of documentation.
You can read more about Leiningen at its home page: http://leiningen.org. Similarly, documentation for Compojure is at its home page, http://compojure.org, and Hiccup is at https://github.com/weavejester/hiccup.
Two good books about Clojure are Programming Clojure by Stuart Halloway and Aaron Bedra (published by the Pragmatic Programmers) and Clojure Programming by Chas Emerick, Brian Carper and Christophe Grand (published by O'Reilly). I've read both during the past year or two, and I enjoyed both of them for different reasons, without a clear preference.