Skip to content

TaskData

patritzenfeld edited this page Feb 10, 2026 · 12 revisions

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.

Generated Data

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
  )

TaskData value

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 the TaskData type need to derive Data from Data.Data (see linked section for instructions).

This is due to how TaskData is stored and accessed in the Autotool instance as described in How Flex-Tasks Work

The Check module

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

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.

Writing the Generator

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.

QuickCheck

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 taskDataGen

Additional type constraints

You 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:

From autotool-capabilities

  • MonadAlloy
  • MonadDiagrams
  • MonadGraphviz

From Control.Monad.Catch

  • MonadCatch
  • MonadThrow

Automated Form Generation

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 form

Let's unpack the unfamiliar types here:

  • Types instancing Formify can be turned into forms. All basic types are already instances of this class.
  • FieldInfo is 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.
  • Widget is a representation of an input form.
  • Rendered is another Flex-Tasks data type. It is the context input forms are composed in.

Example Call

formify
  (Nothing :: Maybe Integer)
  [[single "Input an Integer here!"]]

This would render the following form in Autotool:

Result of the above

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.

Laying Out Form Components

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:

Vertically aligned inputs

While this

formify
  (Just (1,0))
  [[single "Product", single "Sum"]]

would instead render as two inputs next to each other:

Horizontally aligned inputs

Putting it all Together

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
...
|]

💡 getFormData turns the input form into the representation used by the generator.

← Previous: TaskSettings | Next: Check →

Clone this wiki locally