It's been a year since the PeepCode screencast on Clojure (now distributed under PluralSight) 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"]])
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.
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.
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.
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.
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.
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.
๛