Right, I'm trying to get my head around the consequences of
immutability for the sort of programming practices I'm used to, and
how I change the way I do things to fit in with it. Initially I
thought 'well, immutability just means you can't use rplaca and
rplacd, and I've very rarely used either so it doesn't affect me.' But
then I saw that it does. So I'm going right back to one of the first
programs I ever wrote in LISP, which was a multi-player text adventure
game.

To represent a player arriving in a room, you did something like

;; pseudo lisp - actual details unimportant
(defun move (player room)
  (let ((oldroom (get player 'location))(player-terminal (get player
'terminal)))
    (put oldroom 'players (remove player (get oldroom 'players)))
    (map (get oldroom 'players)
      '(lambda (other) (print (list (get player 'name) 'has 'left)
(get other 'terminal)))
    (print (append '(you are in) (get room 'description)) player-
terminal)
    (map (get room 'players)
      '(lambda (other) (print (list (get player 'name) 'has 'arrived)
(get other 'terminal))
        (print (list (get other 'name) 'is 'here) player-terminal)))
    (put player 'location room)
    (put room 'players (cons player (get room 'players)))))

OK, not very functional or elegant. However, it operates mainly by
manipulating property lists, and property lists are  essentially maps,
so at first glance easy to translate... but if the maps are immutable,
then I can't do anything equivalent to

    (put oldroom 'players (remove player (get oldroom 'players)))

I can create a new /copy/ of oldroom which is identical to oldroom
except that it has a different set of players, but I can't then set
all things that pointed to oldroom to point to the new copy. So that
way of working doesn't work. We cannot change state on either the
players or the rooms.

My next thought is that I need a new state-holding data structure
which maps players to rooms and vice-versa:

(def
        #^{:doc "A map which maps every player to his/her location, and
every
                       location to the players who are present there"}
        *player-location-map* {})

(defn
        #^{:doc "Move player, assumed to be a struct of type player,
                        to location, assumed to be a struct of type location"}
        move [player location]
        (let [old-location (player *player-location-map*)]
                ;; do things to announce move to others at new location, and
describe others to player
                ;; do this first because player is not there yet
                (map #(announce-arrival player %) (location 
*player-location-map*))
                (print-to-user player (format "You are in %s" (:description
location))
                (map
                        #(print-to-user player (format "%s is here" (:name %)))
                        (location *player-location-map*))
                (def *player-location-map*
                        (merge
                                {player location}
                                {location (cons player (location 
*player-location-map*)) }
                                {old-location (remove #(identical? player %) 
(old-location *player-
location-map*)) }
                                *player-location-map*))
                ;; do things to announce move to players at old location
                (map #(announce-departure player %) (old-location 
*player-location-
map*)))))

This gives us just one global variable which holds state about who is
where; and, providing the implementation of a map is fairly efficient,
it does so without a huge amount of overhead. I'm always wary of
global variables... It seems (it isn't working yet, for the reason
that follows) that this approach would work. Is it a good one?

However, it raises the next issue. Suppose we want to construct the
old classic maze of twisty little passages, all alike. So we have,
notionally:

(def maze1 {:description "a maze of twisty passages, all alike" :north
maze2 :east maze3 :west maze4})
(def maze2 {:description "a maze of twisty passages, all alike" :north
maze1 :east maze4 :west maze3})

This doesn't work because at the time we try to create maze1, maze2
doesn't exist. If we created maze1 without the reference to maze2 (and
assuming maze3 and maze4 already existed), we could then create maze2.
But we couldn't subsequently fix up the pointer from maze1 to maze2,
because doing so would create a new copy of maze1 and maze2's :north
would still point to the old copy.

Of course we could invent six more magic global maps *north-of*, *east-
of*, *south-of*, *west-of*, *above*, *below*... but this is all
starting to look awfully clunky (yes, OK, we could have one global map
with keys :north-of, :east-of... each pointing to another map, but
that's just hiding the clunkiness behind a facade).

The adventure game's collection of rooms is just an example of the
general case of a cyclic directed graph. Surely there must be some
clean idiomatic way of creating a cyclic graph?

I mean, clearly I can fudge all this by creating Java objects, which
have mutable members (or by using deftype, which, if I understand
correctly, wraps Java objects in some Clojure sugar. But: is that the
idiomatic way out?

Finally, we come to threads and processors.

My original game twenty five years ago used a round robin scheduler
which visited each player in the game, applying the player's move
function to the player; which, if the player were a real human being
sitting at a terminal, meant checking whether there was a line of
input waiting in the buffer, and if there was, interpreting it and
acting on it. But that was a Lisp with a single thread of execution.
With Clojure, in principal, each player (whether or not a real human)
could have their own thread, possibly running in its own process.
Which raises a question about globals and threads.

If everyone lives in the same thread on the same processor, then when
*player-location-map* changes, everyone sees the change. But if two or
more players have different threads but we still have just one global
map holding state, you have the risk that

* Anne starts to construct a new map containing her changes to the
player-location state
* Bill starts to construct a new map containing his changes to the
player-location state
* Bill sets the global *player-location-map* pointer to point to his
new map
* Anne sets the global *player-location-map* pointer to point to her
new map

... and consequently, Bill's changes are just lost. So the global
state map doesn't look like a good solution, after all.

So, there seem to be four possibilities

(1) I've chosen the wrong language for the problem, or vice versa;
(2) There is some idiomatic means of managing mutable state which I've
missed;
(3) The right solution is to create a hybrid system using POJOs for
the mutable state;
(4) The right solution is to create a hybrid system using a relational
database for the mutable state.

Opinions?
-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Reply via email to