When using state machines in hedgehog I have to define a function that updates my model state. Its type should be forall v. Ord1 v => state v -> input v -> Var output v -> state v (see Update constructor of Callback).
Now, I want to get to output, but the only function I've found is concrete, however it's specifying that v of my update function.
How do I define an update function that satisfies the type for Update while still letting me get to the output (presumably by using concrete)?
Ah, I see. What you want to do is use
Varsin your Hedgehog model state and inputs (AKA transitions) wherever a state component depends on earlier actions. You then update the state in terms of these variables abstractly (i.e., in a way that can work both symbolically and concretely). It's only when you execute a command that you make those variables concrete.Let me show you an example. I've used the following imports and extensions, if you want to follow along:
Suppose we have the following mock web API using global IORefs:
In constructing the Hedgehog model, we need to keep in mind that UUIDs will be generated as output by
postFooactions for use in subsequent (get and delete) actions. This dependency of later actions on earlier ones means that these UUIDs should appear as variables in the state.In our state, we'll keep track of a
Mapof UUIDs (as variables) toContentto model the internal state of the database. We'll also keep track of the set of all UUIDs seen even those no longer in the database, so we can test fetching of deleted UUIDs.Now, we'll want to model post, get, and delete commands. To "post", we'll want the following "input" (or transition, or whatever), which posts the given content:
and the corresponding command looks like this:
Note that it's always possible to create a new post, whatever the current state, so
genignores the current state and generate a random post.executetranslates this action into an IO action on the actual API. Note that theUpdatecallback receives the result of thepostFooas a variable. That is,owill have typeVar UUID v. That's fine, because ourUpdatejust needs to store aVar UUID vin the state -- it doesn't need a concreteUUIDvalue because of the way we structured theModelState.We'll also need an
HTraversableinstance forPostfor this to typecheck. SincePostdoesn't have any variables, this instance is trivial:For the "get" input and command, we have:
Here,
genconsults the current state to get the set of ever-observed UUIDs (technically, as symbolic variables). If the set is empty, we don't have any valid UUIDs to test, so noGetis possible, andgenreturnsNothing. Otherwise, we generate aGetrequest for a random UUID (as a symbolic variable) in the set. This may be a UUID still in the database or one that's been deleted. Theexecutemethod then performs the IO action on the actual API. Here, finally, we're allowed to make the variable concrete (which we need to get an actualUUIDfor the API).Note the callbacks -- we
Requirethat the UUID variable be a member of the set of UUID variables in the current state (in case this was invalidated during shrinkage), and after the action executes, weEnsurethat we can retrieve the appropriate content for this UUID. Note that we're allowed to make variables concrete inEnsure, but we didn't need to in this case. NoUpdatewas needed here, sinceGetdoesn't affect the state.We also need an
HTraversableinstance forGet. Since it has a variable, the instance is little more complicated:The code for the "delete" input and command is much the same as for "get", except it has an
Updatecallback.The property we want to test is sequential application of a random collection of these actions. Note that because our API has global state, we need to
resetDatabaseat the start of each test, or things will get bizarre:Finally, then:
and running this gives:
Note that there was one thing we forgot to check above, namely that the API genuinely provides unique UUIDs when posting. For example, if we intentinoally break our UUID generator:
the testing still passes -- the API gives us duplicate UUIDs and we dutifully overwrite old data in our model state, matching the broken API.
To check this, we want to add an
Ensurecallback tos_postto ensure that each new UUID isn't one we've seen before. However if we write:this won't type check, because
ois an actual, concreteUUIDoutput value (i.e., not aVar), butuuids beforeis a set of concrete variables. We could map over the set to extract the concrete values from the variables:or alternatively, we can construct a concrete variable for the value
olike so:Both work fine and catch the buggy
newUuidimplementation above.For reference, the complete code is: