

The Caves of Clojure part 3.1: World Generation - stevelosh
http://stevelosh.com/blog/2012/07/caves-of-clojure-03-1/

======
willismichael
I 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
This 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".

------
monkeyfacebag
I 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.

------
rapala
I 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
I 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
As 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>

~~~
stevelosh
Oh 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!

