masonclj.params
Marshall Abrams
masonclj.params
provides defparams
, a Clojure macro.
masonclj.params/defparams
is a macro with two goals:
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:
gen-class
specification,
which cannot be immediately next to the bean function definitions.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.
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.)
defparams
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.)
defparams
callHere'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.
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)
... ]
...))
defparams
expands toThe 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