How to add "meta" data to a class from gen-class?

#1

What is a good way to pass around additional data with a class created by gen-class, so that I can get hold of them easily given the class name? Currently I am adding an extra method to the class, _opts(), that returns these extra data but it feels as a hack.

(PS: I tried to just store the additional data in an atom but that did not work, perhaps lein test does some namespace cleanup or something between tests. defonce might help but it feels hackish too.)

update Simplified, this is the core of my problem: 1. I am asked to gen-class class named eg math.Algebra and given a map of options. 2. I’m given the class name, “math.Algebra” and asked to return the opts map.

Why?

I am creating clj-concordion to enable using clojure & clojure.test with the Concordion Java test framework that requires me to pass it an object with custom methods. (Each “test” is defined in a Markdown file and I have to provide the underlying fixture object having the methods used in the file, as determined by the writer.) I also need to pass it extra options. The complication is that there are two distinct interactions, from me to the framework and the other way around:

  1. I gen-class and instantiate the fixture object, wrap it with the framework’s FixtureType holding the additional options, and invoke the framework’s run method.
  2. The framework tells me to test a particular .md file - which I map to the corresponding fixture class, use (-> name Class/forName -> .newInstance) to instantiate. Once here - what is the best way to get hold of the options??? (As mentioned above, I get them hackishly from the instance via (._opts obj)

Current implementation

This is how the user defines the fixture class and options:

(cc/deffixture
  "math.algebra.AdditionFixture"
  ; Expose fns in this ns as a method on the object:
  [add]
  ; Extra options and setup/teardown functions:
  {:concordion/full-ognl            false
   :concordion/fail-fast             true
   :concordion/fail-fast-exceptions [IndexOutOfBoundsException]
   ::cc/before-suite                #(println "AdditionFixture: I run before each Suite")
   ::cc/after-suite                 #(println "AdditionFixture: I run after each Suite")})

The deffixture macro then (see line 221 does gen-class, deftest, and defines a wrapper function for each of the methods, including the additional _opts:

; ...
`(do
      ; Add wrapper fns for the methods (defined in let, not shown):
       [email protected]
       (defn ~(symbol (str prefix "_opts")) [~'_] ~opts) ; HERE: For passing opts around w/ the class
       ; Make the class:
       (gen-class
         :name ~class-name
         :methods ~(conj
                     methods*
                     '[_opts [] java.util.Map])
         :prefix ~prefix)
       ; Define a test to run this:
       (test/deftest ~(symbol (str prefix "test"))
         (run-fixture (new-fixture ~(symbol class-name) ~opts) true)))

So, is there a better way? (The framework itself uses Java annotations and thus keeps the additional config and the class at one place.)

Thank you!

#2

Not sure I understand the problem, but at some points it sounds as if you just want to pass around some data in the gen-class instance. If that’s all, gen-class lets you add a :state key whose value is the name of the (one and only) public instance variable that gen-class will define, and :init whose value will be a function that’s called to initialize the state variable when the gen-class instance is created. You can throw a map or some other structure into the state variable to store whatever you want. There are some simple examples on the [https://clojuredocs.org/clojure.core/gen-class](Clojuredocs gen-class page).

1 Like
#3

I will later to explain myself better.
However this solution seems more complicated than my _opts method, as it requires state and init and perhaps a getter?

#4

See the update simplified… in the description. Is it clearer now?

#5

This is one of the things about gen-class that I don’t like. In some contexts, the fact that there can only be one instance variable makes things harder than they need to be. If you feel your :method based approach is simpler, there’s no reason you can’t use it. I think of gen-class as a bit of a hack that has been added on to allow uses that Rich H. doesn’t like. Maybe that’s a misinterpretation, but gen-class is a relatively inconvenient part of a language that often makes programming very convenient.

You don’t need a getter. If your instance is in algeb and the state variable is called state, then (.state algeb) will return whatever is in the state variable. Suppose that the :init function puts the map {:a 42, :b -1} in the state variable. Then you can get the value of the :a key with (:a (.state algeb)).

[If you want to change the contents of the state, you have to use an atom. For example, if you initialize the state with (atom {:a 42 :b -1}), then you can use swap! to change the values of the keys in the map.]

However, maybe you don’t need an instance variable, since you want to get the data from the class name. That sounds like you want the data to be associated with the class and not the instance of it. In that case, my suggestions are not what you need. I think that your approach is probably best, although you will need to make your _opts method static using ^:static, as in this illustration.

1 Like
#6

Thanks a lot for the static trick and details about state! I guess there isn’t better solution than that.