-
Notifications
You must be signed in to change notification settings - Fork 1
TaskData
The TaskData module defines how an individual task instance is created for each student, based on a pseudorandom seed.
Its required interface is the getTask definition,
which generates a unique task setup.
The mandatory function has the following type signature:
getTask :: MonadRandom m => m (
TaskData, -- defined in Global
String, -- the Check module
Rendered Widget -- the input form
)The TaskData value captures what makes each task instance unique.
For example, a randomly generated formula in a logic task.
Both the task description and feedback functions will later use this value to construct individualized output for each student.
⚠️ All components of theTaskDatatype need to deriveDatafrom Data.Data (see linked section for instructions).This is due to how
TaskDatais stored and accessed in the Autotool instance as described in How Flex-Tasks Work
Including the Check module here might seem surprising.
After all, both feedback functions already receive the TaskData value.
So why is it part of the generation context too?
As mentioned in earlier pages, Check is stored as a String first, and this allows you to interpolate values directly into the code.
This allows components of the TaskData value to be interpolated directly into the code,
something that wouldn't be possible otherwise.
This is optional, as some tasks may not have to make use of this feature.
In that case, you can simply forward the Check String as is, without it being dependent on the generated task instance.
The input form is also part of the generation context.
This lets form labels, or even their structure, adapt to the generated task.
For instance, you might want to vary the number of input fields based on the task data.
The Rendered Widget type contains information on POST parameters and the form's HTML, with labels translated to Autotool's supported languages.
Flex-Tasks applies a MonadRandom constraint to the generating data type.
This keeps the interface compatible with a wider range of generation processes than if a specific type were used.
The MonadRandom class has a minimal interface and the library provides few convenience functions.
It is fine to use directly if you only need random numbers or to select an element from a list, for example:
import Control.Monad.Random (MonadRandom(..), uniform)
example :: MonadRandom m => m (Int,Double,String)
example = do
someInt <- getRandomR (1,10) -- from range
someDouble <- getRandom -- any possible value (between 0 and 1)
randomChoice <- uniform ["1","2","3"] -- one of several
pure (intInRange, someDouble, randomChoice)But for something more sophisticated it can become a bit of a hassle.
The QuickCheck library's Gen monad is an alternative for generating a TaskData value.
QuickCheck provides a wider range of supporting functions than MonadRandom,
making Gen more convenient for complex generators.
Gen values can be defined in the same style as MonadRandom.
Here's a simple example that combines several primitives to generate a 4-tuple of values:
import Test.QuickCheck.Gen
type TaskData = (Int, Int, Bool, String)
taskDataGen :: Gen TaskData
taskDataGen = do
int1 <- chooseInt (1,10) -- from range
int2 <- elements [-20,5,90] -- one of several
boolean <- chooseAny -- any possible value
string <- vectorOf 5 $ choose ('A','Z') -- list of 5 values from 'A' to 'Z'
pure (int1,int2,boolean,string)Since Gen is not an instance of MonadRandom, you'll need to convert it to the interface afterwards.
Flex-Tasks provides a function fromGen :: MonadRandom m => Gen a -> m a for this:
import FlexTask.GenUtil (fromGen)
toFlexInterface :: MonadRandom m => m TaskData
toFlexInterface = fromGen taskDataGenYou can add more constraints on type variable m. This will sometimes be necessary if you are trying to reuse code from our existing tasks.
Our library autotool-capabilities offers a set of MonadCapabilities that can be used for a variety of effects, e.g. caching files or employing external programs.
For example, let's say we want to use an instance generating function that requires access to Graphviz. That function will then carry a constraint MonadGraphviz m => ... where functions of that type class allow for communicating with a local Graphviz installation.
This constraint is then also neccessary in the generator's type signature:
import Capabilities.Graphviz (MonadGraphviz)
import Control.Monad.Random (MonadRandom)
withConstraint :: (MonadRandom m, MonadGraphviz m) => m ...
withConstraint = do
result <- functionCallingGraphviz
...Here is a full list of extra constraints you can use on the generator:
MonadAlloyMonadDiagramsMonadGraphviz
MonadCatchMonadThrow
Flex-Tasks can automatically derive input forms from types, helping reduce boilerplate.
The recommended interface for this is the function formify.
This function has the following type signature:
formify
:: Formify a -- type class for turning a type into a form
=> Maybe a -- Optional default value to initially display
-> [[FieldInfo]] -- How to structure the form components
-> Rendered Widget -- Type representing a constructed formLet's unpack the unfamiliar types here:
- Types instancing
Formifycan be turned into forms. All basic types are already instances of this class. -
FieldInfois a Flex-Tasks data type carrying information about what kind of form should be rendered. A nested list is used here to also convey positioning information, i.e. how to lay out the form components. -
Widgetis a representation of an input form. -
Renderedis another Flex-Tasks data type. It is the context input forms are composed in.
formify
(Nothing :: Maybe Integer)
[[single "Input an Integer here!"]]This would render the following form in Autotool:

As you can see, FormInfo is given via builder functions like single.
You’ll find many more in the Flex-Tasks code documentation.
Note that the inline type signature for Nothing is required, otherwise the compiler can’t infer which type should be turned into a form.
The second parameter of formify controls layout: each inner list defines a row of inputs, with fields placed side by side.
Rows stack vertically in order from top to bottom.
This
formify
(Just (1,0))
[[single "Product"], [single "Sum"]]would render as two inputs, one below the other:

While this
formify
(Just (1,0))
[[single "Product", single "Sum"]]would instead render as two inputs next to each other:

Now, we're ready to assemble the required function. We can combine what we learned in the previous sections to write the final generator. Let's use the ingredients already shown on this page as examples:
{-# language QuasiQuotes #-}
getTask :: MonadRandom m => m (TaskData, String, Rendered Widget)
getTask = do
taskData <- fromGen taskDataGen
let myForm = formify (Nothing :: Maybe (Int,Int)) [[single "Product", single "Sum"]]
pure (taskData, check, myForm)
taskDataGen :: Gen TaskData
taskDataGen = ...
check :: String
check = [i|
module Check where
...
|]💡
getFormDataturns the input form into the representation used by the generator.
Author: patrick.ritzenfeld@uni-due.de