in which a year is reflected upon

«

»

It's been a year since the PeepCode screencast on Clojure was released. While it's aged surprisingly well given the relative youth of the Clojure language (1.0 hadn't even been released at the time), there are a few things that could use some updates. I thought it would be helpful for me to step through Mire, the sample project that's built up in the screencast, and update it to reflect the changes that have since occurred in the Clojure ecosystem.

(defproject mire "0.13"
  :description "A multiuser text adventure game/learning project."
  :main mire.server
  :dependencies [[org.clojure/clojure "1.2.0"]
                 [org.clojure/clojure-contrib "1.2.0"]]
  :dev-dependencies [[swank-clojure "1.3.0-SNAPSHOT"]])

Commit 8dd4a5d: Moving to Leiningen

Probably the most obvious thing about the Mire project as it's seen in the screencast that shows its age is its ad-hoc build. (Step 12 of the screencast) At the time there weren't any good ways to build and distribute Clojure projects, so Mire simply contained a copy of Clojure and Contrib in its git repository and included a shell script to perform compilation and packaging. Apart from being just generally tacky, this actually caused the repository to bloat up by 16MB due to the fact that git is really lousy at storing binary files.

Kids these days have it so easy—Leiningen is generally used for managing Clojure projects now. I'm not going to go into detail about this here since it's covered well elsewhere, namely in the readme and tutorial as well as the Leiningen presentation at the Clojure Conj. There are other alternatives, but this is certainly the most straightforward. Leiningen gives you a basic skeleton to work from (lein new), handles dependencies specified in your project file, (lein deps) and creates jar files for you, (lein jar) among other things. In the latest version of leiningen, the lein run task can be used to launch the server.

Commit 1326451b: The player and rooms namespaces: alter-var-root

The main thing that's going on here is replacing a non-toplevel def with a call to alter-var-root. It's never a good idea to call def from within a function. I tried to justify it by the fact that in this case it was a function that was only meant to be called at startup time as the docstring emphasized, (to initialize the rooms map) but it still felt wrong.

The problem is that the rooms map must be loaded from a bunch of files on disk, but the directory to load from isn't known until the -main function is called. So some mutability is called for here, but it's not really enough to justify a ref or an atom since once the server starts it will never change. In the updated version, alter-var-root replaces the def. It takes a var (mire.rooms/rooms in this case) along with a function to apply to the current value of the var and uses the return value as the new root value of the var. It's also possible to simulate change to a var using binding, but this only affects the current thread, and in this case we want the changes to be available to all threads.

Justifiable use of alter-var-root is rare, but startup-time mutability is one of those places it makes sense. For a more idiomatic use of alter-var-root see Robert Hooke or Radagast.

Update: In retrospect, a ref really is suitable here, since there's no reason the player's actions couldn't shift rooms around in some way. So mutability could happen at runtime. Since the mutability needs to be coordinated among players, a ref should be used rather than an atom. See commit 329056.

Commit 0dbd3402: The commands namespace: contrib shuffle

This one is pretty basic; it's mostly just adjusting to the new layout of Clojure Contrib. A number of namespaces got moved: clojure.contrib.duck-streams became clojure.contrib.io, clojure.contrib.str-utils became clojure.contrib.string, etc. In this case we switched from calling clojure.contrib.str-utils/str-join to clojure.contrib.string/join. Some of these namespaces (most of io and some of seq) will be promoted out of Contrib and into Clojure itself before the final 1.2.0 release.

Update: Following the release of Clojure 1.2 I replaced most of the contrib usage with the libraries that were promoted to Clojure. The only remaining use of contrib is the server-sockets library.

The other thing worth noting here is that in the original version of Mire there were a lot of unqualified calls to use, which bring in all the vars from the specified namespace. It's a lot more idiomatic now to either switch to require with :as to alias the namespace to a short name or to stick with use but to limit the list of vars using the :only qualifier to avoid pulling every single thing in, which is the approach taken here. This may seem like a bit of up-front busywork, but makes it easier to track down dependencies between namespaces and fix them in cases like the Contrib upgrade where things get switched around.

Commit 0a0fa0fa: Upgrade server namespace: the resources directory

Here we see more careful use usage along with moving the room data files from data/rooms/ to resources/rooms/ following Leiningen conventions. The resources dir is meant for files that aren't code but are still used by the project, like HTML templates or data files like the rooms that Mire uses. They will get included in the jar file when the project is packaged.

Commit 43ce1f4e: The test suite: clojure.test and use-fixtures

In the Clojure 1.1 release the test-is library got promoted from Contrib into Clojure itself, so that's reflected here. We also move them to a separate test directory to reflect Leiningen convention. The tests for mire.rooms now uses the use-fixtures function, which is a great way to abstract out common setup to be shared among tests.

While OOP test frameworks use setup/teardown methods, the use-fixtures feature of clojure.test takes advantage of the fact that tests themselves are functions. A fixture is simply a function that takes a function argument. In our case the fixture just runs the function inside some bindings, but other common uses of fixtures are to create data on disk in a try/finally block and clean up when it finishes or to conditionally run the tests only if a given network service is accessible. There's a lot of flexibility with clojure.test fixtures.

What Isn't Here

There have been a lot more new features introduced to Clojure since Mire was released. We haven't covered transients, protocols, deftype, or the thrush combinators mostly because these aren't introductory-level topics, but also because they're the trendy new exciting topics and have been covered well elsewhere. Update: two other potentially confusing topics that have gotten a thorough blog treatment since this was written are I/O courtesy of Isaac Hodes and the ns clause courtesy of Colin Jones. I hope this has been enough to modernize the Mire project and help extend the relevance of the screencast and associated codebase. Thanks for tuning in!

Update: the latest commit updates the codebase for Clojure 1.4.0 by removing the last vestige of the old monolithic contrib.

« older | 2010-04-25T05:44:44Z | newer »