Let us say I have a generator users-gen
, that generates a group of 1 or more users. And another parameterized generator called user-actions-gen
that takes a sequence of one or more users and generates a sequence of actions that those users might perform.
(def user-gen
;; generates a user
...)
(def users-gen
;; sequences of 1 or more users
(gen/such-that not-empty (gen/vector gen/users))
(defn user-actions-gen [users]
;; a action performed by some user from the `users argument
...)
If I want to generate a single action for a single sequence of users generated by users-gen, then it is simple, just gen/bind users-gen to user-actions-gen directly.
However, I want to generate many actions from the same sequence of users. I have this problem because I am basically just trying to say "Here is the state, let any random action come in, let us apply the action to the state, let us confirm that the state is still valid; do this for all actions." I have the following code.
(defspec check-that-state-is-always-valid
100
(let [state-atm (atom {})]
(prop/for-all
[[actions users]
(gen/bind users-gen
(fn [users]
(gen/tuple
(gen/vector (user-actions-gen users))
(gen/return users))))]
(doseq [action actions
:let [state (swap! state-atm state-atm-transform-fx action)]]
(is (state-still-valid? state))))))
This sort of works. The problems are that:
- It seems to fully evaluate the doseq rather than halting on the first error
- It just looks kind of wrong. The code is all over the place, it is not entirely evident what it does.
- It seems like maybe user-actions-gen should be taking a generator of users-gen, rather than the realized users value of users-gen? Would this help with composability? Note that I don't want to put them together as users-gen is probably useful to other generators.
So, to recap. I am taking a single generated value from one generator and passing it as an argument to more than one generators. How do I go about doing this in a more attractive/elegant way?
I would make two main changes to what you're currently doing:
Pull out your inline
gen/bind
into a new generator, tentatively namedusers-and-actions-gen
(also note that I swapped the order ofusers
andactions
in the result to match the name):Don't use an atom for testing something which doesn't need it. You can instead generate a lazy sequence of states, and just test that they all have the property you're looking for. That way it will read nicely as well as having the short-circuit property you're looking for:
Other than those two changes, I'm not sure you can really improve on what you're doing. I think your
users-and-actions-gen
is fairly specific. It can be slightly generalised, but I'm not sure the generalisation would be all that useful (it would essentially be a restrictedbind
). What I've proposed above should solve your issues (1) and (2), but I don't think (3) is really an issue.