Liking cljdoc? Tell your friends :D

masonclj.params

Marshall Abrams

masonclj.params provides defparams, a Clojure macro.

Rationale

masonclj.params/defparams is a macro with two goals:

  1. Generate a series of coordinated definitions for model parameters.
  2. Move global configuration data into its own namespace.

1. Coordinating parameter code

The reason for the second goal is that Mason provides a convenient way to allow users to customize configuration variables in the GUI. If you set up certain Java bean-style accessors for your configuation variables, the configuration controls magically appear. However, Mason is designed to be used in a way that's perfectly reasonable in Java, but that Clojure's designers don't really like. Clojure will do what's necessary, but it doesn't go out of its way to make it convenient. As a result, for each variable that you want to be configurable via the GUI, you need to provide:

  1. Two to three bean-ish accessor functions.
  2. Two to three corresponding signatures, in a gen-class specification, which cannot be immediately next to the bean function definitions.
  3. An entry in a data structure such as a defrecord or map, defined somewhere else.
  4. A value for that entry in an intializer function, defined somewhere else again.
  5. Optionally, a commandline option that will allow setting the variable from the command line.

So when you add, delete, or change the definion of a configuration variable, all the above elements have to be kept coordinated. This is inconvenient and bug-prone.

defparams does all of the above for you: You pass it a single line of configuration info, and it does the rest. This means that it does a lot of things in way that's usually hidden, and you just have to know part of what it's doing--see below--but the alternative is worse.

2. A special global-data namespace

defparams defines a map for global parameters. Although some of the methods that deal with it are in your SimState subclass, the map itself is in its own special namespace. The purpose of this to avoid problems with cyclic dependencies when adding many type hints to avoid reflection. Clojure's genclass is its the most flexible way to subclass a Java class, and it seems to be subclassing MASON's SimState class. The problem is that your SimsState subclass will normally register events to be run in SimState's main loop, and these events will call your other Clojure namespaces. So far, so good. In turn these namespaces will probably need to reference some global configuration data, and may update some global data on the model. The natural place to put all of this is in an instance variable of your SimState subclass. (genclass actually allows defining only a single instance variable, but you can put a map or whatever you want in it to store different bits of data.) Now, this would mean that there's a cyclic dependency, because the SimState subclass refers to the other namespaces, and they refer to its data element. Clojure will refuse to compile certain cyclic dependencies, but this kind is OK. That is, it's OK as long as you don't start adding certain sorts of type hints to your code. (Clojure type hints are somewhat like type specifications in statically typed languages such as Java, but they are optional, and uncommon in Clojure. Clojure type hints can speed up code in some situations (and can occasionally make it slower).) For example, if you add type hints referencing your SimState subclass in one of your model's namespaces, and then also add type hints in the SimState subclass referencing the namespace that has a type hint for the subclass, the Clojure compiler will be very unhappy about it. (There are additional remarks about cyclic dependenceis in doc/ClojureMASONinteropTips.md.)

(Note that while there is usually a one-to-one correspondence between Clojure source files and Clojure namespaces, the code generated by defparams switches between namespaces in one file. You can see that below in the macroexpansion of a defparams call below.)

Using defparams

Things you must provide:

defparams requires that the subclass of Mason's SimState be named Sim. (It wouldn't be hard to modify defparams.clj to allow it to discover the name of the SimState subclass, but I don't have a need for this change. If you use defparams, and this bothers you, let me know and I'll fix it.)

The defparams call

Here's an example of use of the defparams macro from masonclj/example/src/example/Sim.clj. (You can see what it expands into below.)

(mp/defparams  [;field name   initial-value type  in gui? with range?
                [num-r-snipes       25      long    [0,500]  ["-R" "Size of r-snipe subpopulation"
                                                              :parse-fn #(Long. %)]]
                [max-energy         20.0    double  true     ["-e" "Maximum energy level for snipes."
                                                              :parse-fn #(Double. %)]]
                [env-width          40      long    [10,250] ["-W" "Width of env.  Must be an even number."
                                                              :parse-fn #(Long. %)]]
                [env-height         40      long    [10,250] ["-H" "Height of env. Must be an even number."
                                                              :parse-fn #(Long. %)]]
                [env-display-size   12.0    double  false    ["-G" "How large to display the env in gui."
                                                              :parse-fn #(Double. %)]]
                [use-gui           false    boolean false    ["-g" "If -g, use GUI; otherwise GUI if +g or no options."
                                                              :parse-fn #(Boolean. %)]]
                [seed               nil     long    false]  ; convenience field to store Sim's seed
                [in-gui           false     boolean false]] ; convenience field to store Boolean re whether in GUI
  :exposes-methods {finish superFinish} ; name for function to call finish() in the superclass
  :methods [[getPopSize [] long]] ; Signatures for Java-visible methods that won't be autogenerated, but be defined below.
 )

The comments above the call describe the elements of the first argument. I show below what this expands to in another section.

For each element in that first argument--a vector of vectors--defparams generates code that performs some or all of the five functions listed in a previous section.

Along the way, defparams defines, in a separate namespace <your prefix>.data, a defrecord named SimData. An instance of this defrecord will be put into the "state" variable of your Sim class instance (i.e. the class that inherits from Mason's sim.engine.SimState). This state variable, named simData, is the only instance variable that Clojure's gen-class allows. We wrap a SimData record in an atom, and make that atom the value of Sim's instance variable simData.

The fields of the SimData defrecord are named by the first elements of the inner vectors in the first argument to defparams.

Values of the fields in this SimData are initialized when your Sim class is created. The initial values are the second elements of the inner vectors in defparams's argument.

The remaining elements in the inner vectors are used to define (a) Bean-style accessor functions that Mason will use to create GUI elements which will allow a user to change the values in the SimData defrecord (using swap! and assoc behind the scenes), and (b) command line options that allow setting these same values. The docstring below says a bit more about this.

Here is defparams's docstring (lightly formatted):

defparams
([fields & addl-gen-class-opts])
Macro
defparams generates Java-bean style and other MASON-style accessors; a gen-class expression in which their signatures are defined along with an instance variable containing a Clojure map for their corresponding values; an initializer function for the map; and a call to clojure.tools.cli/parse-opts to define corresponding commandline options, whose values will be stored in a map in an atom in commandline$. fields is a sequence of 4- or 5-element sequences starting with names of fields in which configuration data will be stored and accessed, followed by initial values and a Java type identifiers for the field. The fourth element is either false to indicate that the field should not be configurable from the GUI, or truthy if it is. In the latter case, it may be a two-element sequence containing default min and max values to be used for sliders in the GUI. (This range doesn't constraint fields' values in any other respect.) The fifth element, if present, specifies short commandline option lists for use by cli-options, except that the second, long option specifier should be left out; it will be generated from the parameter name. The following gen-class options will automatically be provided in the expansion of defparams: :state, :exposes-methods, :init, :main, :methods. Additional options can be provided in addl-gen-class-opts by alternating gen-class option keywords with their intended values. If addl-gen-class-opts includes :exposes-methods or :methods, the value(s) will be combined with automatically generated values for these gen-class options. Note: defparams must be used only in a namespace named <namespace prefix>.Sim, where <namespace prefix> is the path before the last dot of the current namespace. Sim must be aot-compiled in order for gen-class to work.

See the expansion of the above code, below, for details about what defparams does.

Accessing configuration data

You can access the global configuration data in the SimData defrecord stored in the simData instance variable of your class Sim by getting simData from Sim and then defref'ing the atom inside simData. For example, if sim contains your Sim instance:

(let [sim-data$ (.simData sim)
      sim-data @sim-data$
      my-config-param-1 (:my-config-param-1 sim-data)
      my-config-param-2 (:my-config-param-2 sim-data)]
   (do-things-with my-config-param-1 my-config-param-2))

Or you can do it in one step:

   (do-something-with (:my-config-param-3 @(.simData sim)))

For example, I do this in the -start function in Sim.clj.

To use this configuration data, your code obviously has to have had access to the Sim instance at some point. Some examples:

In the -start function in Sim, the Sim instance is passed as the sole argument, which would often be called this.

If your -start function calls or schedules some central routines that run the simulation, you can pass in your Sim instance, the atom wrapping your SimData, or the SimData itself, so that your code can access the configuration data stored in it.

In your GUI class--let's say it's named "GUI"--which inherits from Mason's sim.display.GUIState, the Sim instance will usually be accessible using the getState() accessor that GUI inherits from GUIState. For example, your setup-portrayals function might start like this:

(defn setup-portrayals
  [this-gui]
  (let [sim (.getState this-gui)
        sim-data$ (.simData sim)
           ...                  ]
    ...))

What defparams expands to

The code below shows what the above defparams call from example/src/example/Sim.clj expands into. I generated this code by running lein repl from the toplevel example directory under masonclj, then running:

(in-ns 'example.Sim)
(require '[masonclj.params :as mp])

before passing the defparams expression in Sim.clj, single-quoted, to (clojure.pprint/pprint (macroexpand-1 ...)). I reformatted the output a bit and added comments.

(do
 ;; Define a special namespace for global data with a structure for the data:
 (clojure.core/ns example.data)
 (clojure.core/defrecord SimData [num-r-snipes max-energy env-width
                                  env-height env-display-size use-gui
                                  seed in-gui])

 ;; Now back to the namespace in which defparams was invoked:
 (clojure.core/ns example.Sim
  (:require [example.data]) ; so we can access the global data structure
  (:import example.Sim)     ; weird, but seems to be needed
  ;; gen-class allows us to inherit anything we want from MASON's superclass:
  (:gen-class
   :name example.Sim
   :extends sim.engine.SimState
   :state simData ; gen-class allows only one (!) instance var; it will hold a SimData.
   :exposes-methods {start superStart, finish superFinish} ; superclass method aliases
   :init init-sim-data
   :main true   ; we do want a main()
   ;; Declare methods that we want the MASON GUI to find and invoke:
   :methods [[getNumRSnipes [] long]      
             [setNumRSnipes [long] void]
             [getMaxEnergy [] double]
             [setMaxEnergy [double] void]
             [getEnvWidth [] long]
             [setEnvWidth [long] void]
             [getEnvHeight [] long]
             [setEnvHeight [long] void]
             [domNumRSnipes [] java.lang.Object] ; causes GUI to make sliders
             [domEnvWidth [] java.lang.Object]
             [domEnvHeight [] java.lang.Object]
             [getPopSize [] long]]))
 ;; end of gen-class and ns

 (def commandline$ (atom nil)) ; Used by record-commandline-args! below, and user code.
 
 ;; Function that initializes the simData state var:
 (clojure.core/defn -init-sim-data [seed]
   [[seed] (clojure.core/atom
             (example.data/->SimData 25 20.0 40 40 12.0 false nil false))])

 ;; Now start defining those methods we declared above:
 ;; These cause GUI to make fields displaying data:
 (defn -getNumRSnipes [this] (:num-r-snipes @(.simData this)))
 (defn -getMaxEnergy [this] (:max-energy @(.simData this)))
 (defn -getEnvWidth [this] (:env-width @(.simData this)))
 (defn -getEnvHeight [this] (:env-height @(.simData this)))
 ;; These cause GUI to make the fields editable:
 (defn -setNumRSnipes
   [this newval]
   (clojure.core/swap! (.simData this) clojure.core/assoc :num-r-snipes newval))
 (defn -setMaxEnergy
   [this newval]
   (clojure.core/swap! (.simData this) clojure.core/assoc:max-energy newval))
 (defn -setEnvWidth
   [this newval]
   (clojure.core/swap! (.simData this) clojure.core/assoc :env-width newval))
 (defn -setEnvHeight
   [this newval]
   (clojure.core/swap! (.simData this) clojure.core/assoc :env-height newval))
 ;; If these exist, GUI makes sliders, not just editable fields:
 (defn -domNumRSnipes [this] (Interval. 0 500))
 (defn -domEnvWidth [this] (Interval. 10 250))
 (defn -domEnvHeight [this] (Interval. 10 250))

 ;; Function to parse and store command line args so they can be 
 ;; found later.  It uses clojure clojure.tools/cli to manage the
 ;; command line interaction:.
 (clojure.core/defn record-commandline-args!
  "Temporarily store values of parameters passed on the command line."
  [args__970__auto__]
  (clojure.core/let
    [cli-options [["-?" "--help" "Print this help message."]
                  ["-R" "--num-r-snipes <long> (25)" "Size of r-snipe subpopulation"
                   :parse-fn (fn* [p1__1553#] (Long. p1__1553#))]
                  ["-e" "--max-energy <double> (20.0)" "Maximum energy level for snipes."
                   :parse-fn (fn* [p1__1554#] (Double. p1__1554#))]
                  ["-W" "--env-width <long> (40)" "Width of env.  Must be an even number."
                   :parse-fn (fn* [p1__1555#] (Long. p1__1555#))]
                  ["-H" "--env-height <long> (40)" "Height of env. Must be an even number."
                   :parse-fn (fn* [p1__1556#] (Long. p1__1556#))]
                  ["-G" "--env-display-size <double> (12.0)" "How large to display the env in gui."
                   :parse-fn (fn* [p1__1557#] (Double. p1__1557#))]
                  ["-g" "--use-gui" "If -g, use GUI; otherwise GUI if +g or no options."
                   :parse-fn (fn* [p1__1558#] (Boolean. p1__1558#))]]
     usage-fmt__971__auto__ ; the second let binding
       (clojure.core/fn
         [options]
         (clojure.core/let [fmt-line (clojure.core/fn [[short-opt long-opt desc]]
                                        (clojure.core/str short-opt ", " long-opt ": " desc))]
            (clojure.string/join "\n" (clojure.core/concat (clojure.core/map fmt-line options)))))
     {:as cmdline, :keys [options arguments errors summary]} ; a destructuring let binding
       (clojure.tools.cli/parse-opts args__970__auto__ cli-options)] ; finally the end of the let
       ;; Process the command line args:
       ;; Error handling
       (clojure.core/when errors
         (clojure.core/run! clojure.core/println errors)
         (clojure.core/println "MASON options should appear at the end of the command line after '--'.")
         (java.lang.System/exit 1))
       ;; Help request handling: Second println will print out options and descriptions:
       (clojure.core/when (:help options)
         (clojure.core/println "Command line options (defaults in parentheses):")
         (clojure.core/println (usage-fmt__971__auto__ cli-options))
         (clojure.core/println "MASON options can also be used after these options and after '--'.")
         (clojure.core/println "For example, you can use -for to stop after a specific number of steps.")
         (clojure.core/println "-help (note single dash): Print help message for MASON.")
         (java.lang.System/exit 0))
       ;; store data in example.Sim/commandline$ global so start routines can find it:
       (clojure.core/reset! commandline$ cmdline))))

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close