Changes between Initial Version and Version 1 of Clojure Client Tutorial


Ignore:
Timestamp:
Aug 1, 2011 8:29:05 PM (13 years ago)
Author:
kedwar10
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Clojure Client Tutorial

    v1 v1  
     1== Clojure Client Tutorial ==
     2
     3The first thing you need before using this client is the Leiningen script.  This tool makes managing clojure projects relatively easy by providing a REPL, automatically downloading dependencies, compiling your projects into jar files, and many other useful abilities.  You can download Leiningen and read its tutorial here [[https://github.com/technomancy/leiningen]] and here [[https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md]], respectively. If you're having trouble with leiningen, type "lein help" in the shell to see available commands (you can configure leiningen individually for each project, however, so these will differ depending on which directory you are in). Leiningen supports integration with emacs via the swank/slime mechanism, and if you're bent on using this, you can read up on it here [[http://dev.clojure.org/display/doc/Getting+Started]] (I strongly recommend that you don't attempt this unless you're a emacs expert, as you will likely have to tweak emacs quite a bit to get it working; a simpler solution is to use gvim with the vimclojure script: this provides syntax highlighting and indentation and works out of the box).
     4
     5Once you have the script in a directory of your choice, add the following to your .cshrc file
     6
     7"setenv PATH "$PATH":"/path/to/leiningen/sript".
     8
     9You could skip this step but it will less convenient later on for debugging.
     10
     11Once you have lieningen setup, cd to the client directory and type "tree" into the shell.
     12
     13This organization is the default for the leiningen projects created with the "lein new <myprojectname>" command.
     14
     15docs contains and html file that provides a sort of annotated presentation of the code.  Read this file first to familiarize yourself with the organization of the project. Once you have made your own changes to the client and wish to update the documentation, run "lein marg" in the top level directory.  This invokes the marginalia jar in the lib directory and generates a new html file.
     16
     17In the top level directory there is a file called "project.clj". This file tells leiningen how your project is organized.  For details, consult the tutorial, but know that if you wish to add additional clojure libraries to your project, you will need to specify them here, and run "lein deps" to download them.  Specific directions for leiningen are provided nearly universally for clojure project, so just look them up as needed.
     18
     19src contains all of the clj files in your project.  The organization of the directory must correspond to the namespaces of your project so edit with care (more on this later, but one thing to note is that if you have a namespace with dash in it, "client.my-ns", for example, the actual filename must be "my_ns.clj" and it must be located in the client directory).
     20
     21test contains files that leiningen will use for testing your functions via the "lein test" command.  This functionality is not critical in clojure since you can debug easily from the REPL, but if you want to batch test functions then you can look into this.
     22
     23When you create a new project with leiningen, it will automatically provide a core.clj file for you.  If your namespaces are organized into a tree, this is the root.  When you run the REPL in the top level directory via the "lein repl" command, the core namspace becomes available to you.  This is why having leiningen on your path is useful, as you won't always want to load the core namespace.  An important detail: namespaces must NOT be cyclic in clojure, so plan accordingly when designing your project.
     24
     25At this point I recommend reading "Joy of Clojure", by Michael Fogus and Chris Houser. This is an excellent introduction to clojure if you are already familiar with lisp or scheme.  Much of the inspiration for this client comes from this book so if you're having trouble figuring out what is going on, this will provide most of the background you will need.  If you're short on time or funds, just search for online tutorials, there are plenty out there.  If you need to quickly look up the documentation on any particular function, just type "(doc f)" in the REPL where f is the function (This will also work for functions that you define provided that you wrote docstrings or added meta-data).
     26
     27Some advice for clojure programming:
     28* There is no solid way to debug clojure code, so stick to small functions that you can test in the REPL.
     29* Leiningen provides "compile" and "run" commands: NEVER USE THESE (unless you want java interop, then you must be careful to recompile the function you wish to export, before running them in the REPL).
     30* If you define a record in a namespace and wish to access it in another, you must explicitly import it with the ":import" keyword in your namespace declaration.
     31* Giving names to your anonymous functions will dramatically increase the usefulness of clojure error reporting (an example: (fn identity [x] x).  Doing this will also allow the function to call itself.  Sometimes you can get slightly better error reporting if you compile your project; if you do this, run "lein clean" immediately afterwards.
     32* If you want to ensure tail cail optimization, use the "recur" function instead of using your function name.  "recur" also works with loops.
     33* "Weird" error messages usually result from forgetting to specify the arguments during a function definition or from mismatched parenthesis.
     34
     35Now it is time to learn to use the client.  cd to the top level directory and typu "lein repl". If everything went smoothly you'll see something about no rlwrap and the prompt.
     36
     37$ lein repl
     38which: no rlwrap in (/usr/staff/bin:/usr/ccs/bin:/opt/SUNWspro/bin:/bin:/usr/bin:/usr/openwin/bin:/usr/dt/bin:/usr/bin/X11:/usr/gfx:/usr/local/bin:/usr/sbin:/usr/ucb:/usr/bsd:/usr/vision/bin:/usr/grads/bin:/usr/java/bin:/u/kedwar10/bin:/home/vax7/u21/kedwar10/Software/cake/bin:/home/vax7/u21/kedwar10/Software:/home/vax7/u21/kedwar10/Software/LPG-td-1.0/:/home/vax7/u21/kedwar10/Software/q3map2/)
     39REPL started; server listening on localhost:2192.
     40client.core=>
     41
     42You can launch ioquake with a specific map using the "ioquake" (located in utilities.clj). Type (doc ioquake) to see the documentation.
     43
     44client.core=> (doc run-ioquake)
     45client.utilities/run-ioquake
     46([level] [path level])
     47  This function launches ioquake with the specified level, if the path to ioquake is hardcoded into this function, type '(run-ioquake <level>)' where <level> is a map in the maps folder of ioquake, else you must provide the path.
     48nil
     49
     50For example:
     51client.core=> (run-ioquake "/home/vax7/u21/kedwar10/Projects/Quagents/quagents/ioquake3/build/release-linux-i386/ioquake3.i386" "firstroom")
     52#<UNIXProcess java.lang.UNIXProcess@2a5ab9>
     53
     54Since I already have the path predefined, however, this will also work:
     55client.core=> (run-ioquake "firstroom")                                                                                                     
     56#<UNIXProcess java.lang.UNIXProcess@56c3cf>
     57
     58You should now see ioquake running in a separate window (if you are in fullscreen mode, navigate to setup and switch this off).  Now hit
     59backtick (`) to free the mouse from ioquake.  Back in the REPL, type "(doc load-quagent)"
     60client.core=> (doc load-quagent)
     61-------------------------
     62client.commands/load-quagent
     63([] [moniker])
     64  This function will load a quagent into the virtual environment.  Takes a optional 'moniker' (name is a reserved word in clojure) argument that specifies the key that can be used to identify the quagent and get information about it.  Otherwise a randomly generated name will be made with the prefix 'quagent'.
     65nil
     66
     67Let's load a quagent into the map (note that if there is only one spawnpoint on the map, you'll want to move forward a bit in order to avoid getting telefragged).
     68client.core=> (load-quagent)                                                                                                               
     69:quagent240
     70client.core=> (load-quagent "Bob")
     71:Bob
     72
     73You can check which quagents are currently loaded with the 'get-quagents' function.
     74client.core=> (get-quagents)
     75(:Bob :quagent240)
     76
     77You can use these keywords to make the quagents do things in the environment (Note: this functions are subject to change).
     78client.core=> (face-me :Bob)
     79[]
     80client.core=> (come-here :Bob)
     81[]
     82Note that control of the terminal does not return until the bot has completed his action.
     83
     84Now say you want to define a function on the fly to send an op to the server, the easiest way to do so is the 'client->server' function.
     85client.core=> (doc client->server)
     86-------------------------
     87client.protocol-one/client->server
     88([send-func quagent scheduling op args & fs])
     89  Provides a usable interface for sending ops to the server.  Arguments are the type of send function, the quagent to execute the op, the scheduling, the op keyword (see *codes* in protocol_one.clj) the arguments to the op (in vector form) and the dispatch functions to be given to the send function.
     90nil
     91
     92The send function can be one of the following
     93
     94client.core=> (doc send-and-forget)
     95-------------------------
     96client.protocol-one/send-and-forget
     97([quagent id msg])
     98  Send an op to the server and throw away the results.
     99nil
     100
     101client.core=> (doc send-and-get)   
     102-------------------------
     103client.protocol-one/send-and-get
     104([quagent id msg] [quagent id msg f])
     105  Send a op to the server and block this thread until the op completes. Args are a (unique) op id, a msg to send to the server and a function f that determines how to combine the current reply from the server with past results (defaults to conj).
     106nil
     107
     108client.core=> (doc send-and-get-later)
     109-------------------------
     110client.protocol-one/send-and-get-later
     111([quagent id msg] [quagent id msg f])
     112  This will spawn a separate thread to do the send and get, if you try to dereference it before it has completed, it will block.  Use future-done? to check the status before dereferencing.
     113nil
     114
     115client.core=> (doc send-and-watch)   
     116-------------------------
     117client.protocol-one/send-and-watch
     118([quagent id msg wf] [quagent id msg f wf])
     119  Sends the op to the server and then applies f to update the value.  When the value changes wf is called (wf must meet the requirements of the add-watch function).  The return value is the key for the watcher; this is used to delete it if necessary [(example)](http://clojure-examples.appspot.com/clojure.core/add-watch?revision=1278516003572).  No ops of this type are currently supported by the server.
     120nil
     121
     122Say we want to define a function "get-location-of" that takes one argument (a quagent key) and returns the position as a vector of doubles.
     123From protocol zero, we know that the string to be sent to the server should look like "n lc <id>" where <id> is the op id. One possible implementation would be
     124 
     125client.core=> (defn get-location-of [quagent] (client->server send-and-get quagent :now :current-location nil))
     126#'client.core/get-location-of
     127
     128Note that the protocol zero op codes have been mapped to more user friendly keywords.  Here is the full map (subject to change)
     129* :move-indefinitely "mi"
     130* :move-for "mf"
     131* :move-by "mb"
     132* :move-to "mt"
     133* :jump-once "ju"
     134* :rotate "ro"
     135* :fire-weapon "fw"
     136* :switch-weapon "sw"
     137* :set-crouch "sc"
     138* :shove "sv"
     139* :say "sy"
     140* :pick-up "pu"
     141* :put-down "pd"
     142* :current-health "hc"
     143* :max-health "hm"
     144* :current-armor "ac"
     145* :max-armor "am"
     146* :current-location "lc"
     147* :current-facing "fc"
     148* :can-see "cs"
     149* :radar "ra"
     150* :what-is "wi"
     151* :current-ammo "mc"
     152* :range-finder "rf"
     153* :check-inventory "ci"
     154* :follow "fo"
     155* :batch-range-finder "rb"
     156* :pop "po"
     157* :pause "pa"
     158* :forget-all-tasks "fa"
     159* :skip "sk"
     160* :peek "pk"
     161* :now "n"
     162* :then "t"
     163* :replace "r"
     164
     165Running this however, didn't yield the expected output.
     166client.core=> (get-location-of :Bob)
     167[{:data ("-128.000000" "136.000000" "40.125000"), :op "lc", :reply-type "rs", :id "1"}]
     168
     169Instead of getting a vector of doubles, it returned a vector that contains a map.  To fix this we need to pass in
     170an dispatch function to control the behavior of the return value. This function takes two arguments, the previous
     171result, and the most recent reply from the server, and determines how to combine them. For example, say we only
     172wish to return the map.
     173
     174client.core=> (defn get-location-of [quagent] (client->server send-and-get quagent :now :current-location nil (fn [prev reply] reply)))
     175#'client.core/get-location-of
     176
     177Note the change in return value.
     178client.core=> (get-location-of :Bob)                                                                                                 
     179{:data ("-128.000000" "136.000000" "40.125000"), :op "lc", :reply-type "rs", :id "2"}
     180
     181Here is the correct version. Note the use of destructuring in the anonymous function
     182client.core=> (defn get-location-of [quagent] (client->server send-and-get quagent :now :current-location nil (fn [prev {:keys [data]}] (vec(map #(Double/parseDouble %) data)))))
     183#'client.core/get-location-of
     184
     185Now this returns the expected output.
     186client.core=> (get-location-of :Bob)
     187[-128.0 136.0 40.125]
     188
     189This particular anonymous function is so frequent that that it comes predefined.
     190client.core=> (defn get-location-of [quagent] (client->server send-and-get quagent :now :current-location ni data->doubles))                           
     191#'client.core/get-location-of
     192
     193Suppose that this particular op took a long time to complete and we didn't want to wait around for it to complete,
     194The way clojure handles this is through the use of "futures".  Simply, clojure will do the computation in a separate
     195thread so the current one can continue.  send-and-get-later is identitical to send-and-get except that it is passed
     196to the "future" function.
     197client.core=> (doc future)
     198-------------------------
     199clojure.core/future
     200([& body])
     201Macro
     202  Takes a body of expressions and yields a future object that will
     203  invoke the body in another thread, and will cache the result and
     204  return it on all subsequent calls to deref/@. If the computation has
     205  not yet finished, calls to deref/@ will block.
     206nil
     207
     208We need to redefine "get-location-of" slightly.
     209client.core=> (defn get-location-of [quagent] (client->server send-and-get-later quagent :now :current-location nil data->doubles))
     210#'client.core/get-location-of
     211
     212Now we need a variable to store the future.
     213client.core=> (def f (get-location-of :Bob))
     214#'client.core/f
     215
     216In order to check if the computation is done use the "future-done?" function.
     217client.core=> (doc future-done?)
     218-------------------------
     219clojure.core/future-done?
     220([f])
     221  Returns true if future f is done
     222nil
     223
     224client.core=> (future-done? f)
     225true
     226
     227Now we know f is safe to dereference.
     228client.core=> @f
     229[-128.0 136.0 40.125]
     230
     231Futures allow us to do more interesting things, however. Say we want to make two bots explore a maze simultaneously,
     232this can't be done if the quagents are all blocking on the same thread. Type control-c in the repl and execute the
     233following.
     234client.core=> (run-ioquake "largemaze")   
     235#<UNIXProcess java.lang.UNIXProcess@5ce40>
     236client.core=> (load-quagent :Bob) ; remember to stand in the corner         
     237:Bob
     238client.core=>(make-gui) ; the quagents use a value iteration to calculate the best policy; brightness denotes higher utility                           
     239#<JFrame javax.swing.JFrame[frame0,0,0,510x530,layout=java.awt.BorderLayout,title=Quagent Utility Map,resizable,normal,defaultCloseOperation=EXIT_ON_CLOSE,rootPane=javax.swing.JRootPane[,5,25,500x500,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]>
     240client.core=> (def q1 (future (explore-maze :Bob)))
     241#'client.core/q1
     242client.core=> (load-quagent :Joe)                 
     243:Joe
     244client.core=> (def q2 (future (explore-maze :Joe)))
     245#'client.core/q2
     246
     247Some time later while still exploring the maze...
     248client.core=> (future-done? q1)
     249true
     250client.core=> (future-done? q2)
     251false
     252
     253Looks like something went wrong with q1 (the server sometimes stops responding after many calls).  Fortunately this is no problem for futures as they can be restarted.
     254client.core=> (future-cancel q1)
     255false
     256client.core=> (def q1 (future (explore-maze :Bob)))
     257#'client.core/q1
     258
     259Try running these without the future call and note the difference. 
     260
     261Suppose now we need a more sophisticated behavior whereby the quagent sends out a message to the server and then calls a function every
     262time the server replies before terminating the op.  This can be accomplished with the last send function "send-and-watch".  Here, an
     263additional watcher function (wf) must be specified that takes four arguments: a (k)ey, (r)efernce, (o)ld value, and (n)ew value.
     264client.core=> (defn get-location-of [quagent] (client->server send-and-watch quagent :now :current-location nil (fn [k r o n](println "old val " o " new val " n)))
     265#'client.core/get-location-of
     266client.core=> (get-location-of :Joe)                                                                                                                               
     267:watcher245
     268old val  []  new val  [{:data (64.000000 64.000000 32.125000), :op lc, :reply-type rs, :id 1}]
     269
     270The key :watcher245 can be used to delete the watch function if so desired.  Otherwise it will be removed when the command completes.
     271client.core=> (doc remove-watch)
     272-------------------------
     273clojure.core/remove-watch
     274([reference key])
     275  Alpha - subject to change.
     276  Removes a watch (set by add-watch) from a reference
     277nil
     278
     279
     280
     281
     282
     283