5 ms·
The Caves of Clojure part 3.1: World Generation
- monkeyfacebag 14y agoI don't recall if this was mentioned in the other threads on this project, but I would highly recommend the section in Chapter 3 of the really fantastic "Clojure Programming" where the authors step through the processes of converting various programs from an imperative to a functional style. This was really my aha! moment with Clojure.
- willismichael 14y agoI didn't see anywhere to comment on your blog, but after reading part 2 I had a idea about solving the inventory-select problem. What if instead of the UI "returning a value", it instead has a property that is a function describing what to do upon selecting inventory, something like this: [{:kind :play} {:kind :throw} {:kind :inventory-select :on-select (fn [game selected-item] ; code here that returns an altered game: ; * puts selected item into throw UI ; * pushes targeting UI onto the stack ) }] That way whenever something creates the inventory UI, it can pass in a function that specifies what to do.
- stevelosh 14y agoThis is basically the "pass a callback" approach that's popular in the JS world. Down this road lies madness. Let's see how this might actually work. First the easy part, the inventory selector. It takes the user's input and figures out what item they picked, then calls the callback: (defn pop-ui [game] (update-in game [:uis] butlast)) (defn push-ui [game ui] (update-in game [:uis] conj ui)) (defmethod process-input :inventory-select [ui game input] (let [selected-item (get-item game input) callback (:on-select ui)] (callback (pop-ui game) selected-item))) That's not too bad. It calls the callback with the game (after popping itself off the UI stack) and the item they picked. Now to the main play UI that's going to set up the throwing when the user presses t: (defmethod process-input :play [ui game input] ; ... \t (update-in game [:uis] concat (make-throw-ui-stack)) ; ... ) Also pretty clean. We're going to be appending two UIs to the stack (throw and inventory select) so we use concat, but that's fine. You could have another multimethod if you really didn't want the play UI to know anything about targeting, but that's irrelvant for now. So far so good, but now it's time to dive down the rabbit hole: (defn make-throw-ui-stack [] [(new UI :throw) (new UI :inventory-select :on-select (fn [game selected-item] ,,,))]) Okay so we have the basic stack of UIs we'll need to start, all that's left is the callback. Well it needs to: * Take the game and the item that was selected. * Return a game with the throw UI storing the item for later, and a targeting UI after that so the user can pick a monster to kill. That doesn't sound so bad: (defn make-throw-ui-stack [] [(new UI :throw) (new UI :inventory-select :on-select (fn [game selected-item] (push-ui (new UI :target))))]) Okay, a bit indentation-heavy but readable. At least we don't have to store the selected-item in the throw UI any more, because our callback closes over it. Let's make the targeting UI: (defmethod process-input :target [ui game input] (let [selected-monster (get-monster game input) callback (:on-select ui)] (callback (pop-ui game) selected-monster))) Cool, but wait, now we need another callback in the throw UI! (defn make-throw-ui-stack [] [(new UI :throw) (new UI :inventory-select :on-select (fn [game selected-item] (push-ui (new UI :target :on-select (fn [game selected-item] (let [game (pop-ui game)] (throw-item-at game selected-item selected-monster)))))))]) Okay this looks pretty bad. It's ugly, and all the logic is in the "make throw UI stack" call. Now notice that we haven't handled the "user pressed escape to cancel" case at all. Oh god. Contrast this to the "reduce over input" method: (defn pop-ui [game] (update-in game [:uis] butlast)) (defn push-ui [game ui] (update-in game [:uis] conj ui)) (defmethod process-input :inventory-select [ui game input] (let [result (if (= input :escape) :cancel (get-item game input))] (-> game pop-ui (assoc :input {:item result}))) (defmethod process-input :target [ui game input] (let [result (if (= input :escape) :cancel (get-monster game input))] (-> game pop-ui (assoc :input {:monster result}))) (defmethod process-input :throw [ui game item] (cond (= input :escape) (pop-ui game) (contains? input :item) (-> game pop-ui (push-ui (assoc ui :item (:item input))) (push-ui (new UI :target))) (contains? input :monster) (throw-item-at game (:item ui) (:monster input)))) (defn make-throw-ui-stack [] [(new UI :throw) (new UI :inventory-select)]) (defmethod process-input :play [ui game input] ; ... \t (update-in game [:uis] concat (make-throw-ui-stack)) ; ... ) Not only do we now handle escaping from the menus, but this looks much cleaner to me because we're no longer nesting functions inside each other. We've also decoupled the "make the throwing UI" from the "game logic of the throwing UI".
- rapala 14y agoI find it odd that he is using the new macro to create the record instances. I thought that it is meant to be used only in Java inter-op, as it requires that the class of the record is visible in the current namespace.
- stevelosh 14y agoI don't get what you mean. I like the way new looks as compared to the Record. syntax, but either way you need the java class visible in the current namespace: sjl at grendel in ~/s/caves ><((°> lein2 repl caves.core=> (use 'caves.world) nil caves.core=> (new World [:foo]) CompilerException java.lang.IllegalArgumentException: Unable to resolve classname: World, compiling:(NO_SOURCE_PATH:1) caves.core=> (import '[caves.world World]) caves.world.World caves.core=> (new World [:foo]) #caves.world.World{:tiles [:foo]} caves.core=> Bye for now! sjl at grendel in ~/s/caves ><((°> lein2 repl caves.core=> (use 'caves.world) nil caves.core=> (World. [:foo]) CompilerException java.lang.IllegalArgumentException: Unable to resolve classname: World, compiling:(NO_SOURCE_PATH:1) caves.core=> Is there another way to create record instances that doesn't involve that warty (import) call that I'm not aware of?
- bitsai 14y agoAs of Clojure 1.3, defrecord (and deftype) automatically define factory functions in the same namespace as the record itself. defrecord generates 2 such functions, one that takes positional arguments and one that takes a map. So once you have :use'd or :require'd the namespace, you can do stuff like the following without having to import anymore: (->World [:foo]) (->World {:tiles [:foo]}) More details here: http://dev.clojure.org/display/design/defrecord+improvements http://dev.clojure.org/display/design/defrecord+improvements
- stevelosh 14y agoOh cool, I didn't know that. I kind of blocked 1.3 out of my memory after the "let's remove clojure.contrib and break everyone's shit" mess. It's good to know that progress toward patching all the places Java tends to leak through into Clojure is happening. I can't say I'm a fan of the punctuation-laden syntax, but I'll pick my battles and not complain about it. I'll switch over to this new form tonight and mention it in one of the later posts. Thanks!