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