The first command we'd want to have is a command that tells us about the location we're standing in. So what would a function need to describe a location in a world? Well, it would need to know the location we want to describe and would need to be able to look at a map and find that location on the map. Here's our function, and it does exactly that:
(defn describe-location [location game-map] (first (location game-map)))
The word defn
means, as you'd expect, that we're
defining a function. The name of the function is
describe-location
and it takes two parameters:
a location and a game map. Since these variables appear in the
function's parameters, it means they are local and hence unrelated
to the global location
and game-map
variables we defined earlier. Note that functions in Lisp are often more like
functions in math than in other programming languages: Just
like in math, this function does not print stuff for the user
to read or pop up a message box: All it does is return a value
as a result of the function that contains the description.
Let's imagine our location is in the living-room (which, indeed,
it is...).
Clojure expects the parameter lists in square brackets. Most other Lisps expect parentheses here.
To find the description for this, our describe-location
first needs to look up the
spot in the map that points to the living-room.
(location game-map)
performs the lookup on the
game-map
hash map and then returns the data
describing the living-room. Then the command first
trims out the first item in that list, which is the description
of the living-room (If you look at the game-map
variable we had created, the snippet of text describing the
living-room was the second item in the list that contained
all the data about the living room...)
Now let's use our Lisp prompt to test our function - Again, like all the text in
'(this font and color)
in the tutorial, paste the following text into your Lisp prompt:
(describe-location 'living-room game-map)
user=> (describe-location 'living-room game-map) (you are in the living-room of a wizard's house - there is a wizard snoring loudly on the couch -)
Perfect! Just what we wanted... Notice how we put a quote
in front of the symbol living-room
, since
this symbol is just a piece of data naming the location
(i.e. we want it read in Data Mode) , but how we
didn't put a quote in front of the symbol
game-map
, since in this case we want the
list compiler to hunt down the data stored in the
game-map
variable (i.e. we want the compiler
to be in Code Mode and not just look at the word
game-map
as a chunk of raw data)
You may have noticed that our describe-location
function seems
pretty awkward in several different ways. First of all, why are
we passing in the variables for location and map as parameters,
instead of just reading our global variables directly? The reason
is that Lispers often like to write code in the
Functional Programming Style (To be clear, this is
completely unrelated in any way to the concept called
"procedural programming" or "structural programming" that you
might have learned about in high school...). In this style,
the goal is to write functions that always follow the following
rules:
- You only read variables that are passed into the function or are created by the function (So you don't read any global variables)
- You never change the value of a variable that has already been set (So no incrementing variables or other such foolishness)
- You never interact with the outside world, besides returning a result value. (So no writing to files, no writing messages for the user)
You may be wondering if you can actually write any code like this that actually does anything useful, given these brutal restrictions... the answer is yes, once you get used to the style... Why would anyone bother following these rules? One very important reason: Writing code in this style gives your program referential transparency: This means that a given piece of code, called with the same parameters, always positively returns the same result and does exactly the same thing no matter when you call it - This can reduce programming errors and is believed to improve programmer productivity in many cases.
Of course, you'll always have some functions that are not functional in style or you couldn't communicate with the user or other parts of the outside world. Most of the functions later in this tutorial do not follow these rules.
Another problem with our describe-location
function is that it does not tell us about the paths in and
out of the location to other locations. Let's write a function
that describes these paths:
(defn describe-path [path] `(there is a ~(second path) going ~(first path) from here -))
Ok, now this function looks pretty strange: It almost looks more like a piece of data than a function. Let's try it out first and figures out how it does what it does later:
(describe-path '(west door garden))
user=> (describe-path '(west door garden)) (user/there user/is user/a door user/going west user/from user/here clojure.core/-)
What is that !? The result is now cluttered with strange '/' characters and extra words ! This is because Clojure adds namespace information in to expressions that begin with a backquote. We won't go into detail here, but instead provide you with a way to remove that confusing output:
(defn spel-print [list] (map (fn [x] (symbol (name x))) list))and enter
(spel-print (describe-path '(west door garden)))
user=> (spel-print (describe-path '(west door garden))) (there is a door going west from here -)
Clojure namespaces are out of the scope of this tutorial. They are an important concept to Clojure, so we recommend to read about them in the language documentation.
So now it's clear: This function takes a list describing a
path (just like we have inside our game-map
variable) and makes
a nice sentence out of it. Now when we look at the function
again, we can see that the function "looks" a lot like the
data it produces: It basically just splices the first and
second item from the path into a declared sentence. How does
it do this? It uses back-quoting !
Remember that we've used a quote before to flip the compiler from Code Mode to Data Mode - Well, by using the back-quote (the quote in the upper left corner of the keyboard) we can not only flip, but then also flop back into Code Mode by using a tilde ("~") character:
This "back-quoting" technique is a great feature in Lisp - it lets us write code that looks just like the data it creates. This happens frequently with code written in a functional style: By building functions that look like the data they create, we can make our code easier to understand and also build for longevity: As long as the data doesn't change, the functions will probably not need to be refactored or otherwise changed, since they mirror the data so closely. Imagine how you'd write a function like this in VB or C: You would probably chop the path into pieces, then append the text snippets and the pieces together again - A more haphazard process that "looks" totally different from the data that is created and probably less likely to have longevity.
Now we can describe a path, but a location in our game
may have more than one path, so let's create a function
called describe-paths
:
(defn describe-paths [location game-map] (apply concat (map describe-path (rest (get game-map location)))))
This function uses another common functional
programming technique: The use of Higher Order
Functions - This means that the apply
and map
functions are taking other functions
as parameters so that they can call them themselves -
map
simply applies another
function to every object in the list, basically causing all
paths to be changed into pretty descriptions by the
describe-path
function. The "apply concat"
just cleans out some parentheses and isn't so important.
Let's try this new function:
(spel-print (describe-paths 'living-room game-map))
user=> (spel-print (describe-paths 'living-room game-map)) (there is a door going west from here - there is a stairway going upstairs from here -)
Beautiful!
We still have one thing we need to describe: If there are any objects on the floor at the location we are standing in, we'll want to describe them as well. Let's first write a helper function that tells us wether an item is in a given place:
(defn is-at? [obj loc obj-loc] (= (obj obj-loc) loc))
...the =
function tells us if the symbol from the object
location list is the same as the current location.
Let's try this out:
(is-at? 'whiskey-bottle 'living-room object-locations)
user=> (is-at? 'whiskey-bottle 'living-room object-locations) true
The symbol true
(or any value other than
nil
) means that it's true that the whiskey-bottle
is in living-room.
Now let's use this function to describe the floor:
(defn describe-floor [loc objs obj-loc] (apply concat (map (fn [x] `(you see a ~x on the floor -)) (filter (fn [x] (is-at? x loc obj-loc)) objs))))
This function has a couple of new things: First of all,
it has anonymous functions (fn
is the command for creating such a function.) That first fn
form is just the same
as defining a helper function:
(defn blabla [x] `(you see a ~x on the floor.))
and then sending
blabla
to the map function. The filter
function
in this case is filtering out any objects from the list that are not at the current
location before passing the list on to map to build pretty
sentences. Let's try this new function:
(spel-print (describe-floor 'living-room objects object-locations))
user=> (spel-print (describe-floor 'living-room objects object-locations)) (you see a whiskey-bottle on the floor - you see a bucket on the floor -)
Now we can tie all these descriptor functions into a single,
easy command called look
that uses the global
variables (therefore this function is not in the
Functional Style) to feed all the descriptor functions
and describes everything:
(defn look [] (spel-print (concat (describe-location location game-map) (describe-paths location game-map) (describe-floor location objects object-locations))))
Let's try it:
user=> (look) (you are in the living room of a wizards house - there is a wizard snoring loudly on the couch - there is a door going west from here - there is a stairway going upstairs from here - you see a whiskey-bottle on the floor - you see a bucket on the floor -)
pretty cool!