-
Notifications
You must be signed in to change notification settings - Fork 1
Parse
Now for the final required module: Parse.
Here, you must define the function parseSubmission, which turns the submission (encoded as a String) into a value of type Submission.
This value is then forwarded to the feedback functions.
The required function has the following type signature:
parseSubmission :: (Monad m, OutputCapable (ReportT o m)) => String -> LangM' (ReportT o m) SubmissionThis is similar to what we’ve seen before, yet also quite a bit different.
The usual OutputCapable constraint is here, but it is being applied to an unfamiliar type ReportT.
That same type also appears in the result, alongside another new one: LangM'.
Let's break them down:
-
ReportTis a type fromoutput-blocksthat organizes output components. It fills themin the kind ofOutputCapable m => Lang msignatures we’ve seen so far. In fact, pretty much all actual instances ofOutputCapableare of the formReportT OutputType ExecutionMonad. So even though this signature may look more restrictive than those with a plainm, it’s practically equivalent. -
LangM'is the more general form ofLangMandRated. WhereasLangMhas no return value andRatedcan only return a score,LangM'can return any type. The types are thus defined:type LangM m = LangM' m ()andtype Rated m = LangM' m Rational.
The LangM' part is easier to explain:
We want to parse the submission, but maybe something goes wrong.
In case we cannot parse what a student sent in, a ParseError would happen.
If we would simply operate on such a simplified version of the parser,
then we’d be stuck trying to turn that ParseError into understandable feedback for the student.
But why do that when we have OutputCapable at our disposal?
So instead we run the parser inside the usual output blocks context and chain that together with the feedback functions in Check.
This means we need to embed the parsed value as a result
and that is only possible by using LangM' directly, since the other two type synonyms have a fixed result type.
Now we can use the usual building blocks to display something more sophisticated if a ParseError occurs.
This also reduces the amount of duplicate work we have to do.
We could just forward the ParseError to both checkSyntax and checkSemantics, then we would also have the usual building blocks to use there.
But we would need to deal with processing that error two times separately.
When embedding the value, we only need to deal with it once.
The ReportT part is a bit more obscure.
One of the functions used behind the scenes (to maintain type consistency when a ParseError occurs) requires this more explicit type structure instead of a simple m constraint.
This unfortunately floats upwards and surfaces at this point.
You’ll just have to take my word for it: Even though the signature looks a bit intimidating, the additional types don’t change much compared to what we’ve seen before.
It is pretty much the same as the other functions involving OutputCapable, but we are returning the parsed submission instead of a score.
To specify the required interface, you need two ingredients:
- a
Parsecparser for theSubmissiontype - Some
LangM'wrapper function for giving prettier output on errors
Parsec is a library for parser combinators. Usage is mostly intuitive and can be written via do-notation: Here's a simple example illustrating the syntax:
import Text.Parsec (char, digit, many1, optionMaybe)
import Text.Parsec.String (Parser)
myIntegerParser :: Parser Integer
myIntegerParser = do
mNegation <- optionMaybe $ char '-'
number <- many1 $ digit
pure $ read $
case mNegation of
Nothing -> number
Just neg -> neg : numberYou can define your own parser like this, but in most cases that won’t be necessary. Instead, Flex-Tasks provides parsers for basic types and also derives parsers for more complex types automatically for you. The interface for this is given in the following function:
formParser :: Parse a => Parser aWhere Parse is the Flex-Tasks type class for automatic parser generation.
This can be used directly for basic types, but requires deriving an instance for custom ones:
data Guy = Guy { name :: String, age :: Int}
instance Parse GuyAfterwards, you can use formParser for this imaginary Guy type.
For the second part, Flex-Tasks provides a few helper functions. Most of the time, you’ll want to use one of the following:
-- Will display an error with information on which input field caused the problem.
parseSubmission = parseWithOrReport formParser reportWithFieldNumber
{-
Takes just the parser and assumes nothing can go wrong.
Will fail if input can't be parsed.
This can be used if the submission cannot possibly cause an error,
e.g. with checkbox inputs.
-}
parseSubmission = parseInfallibly formParserAuthor: patrick.ritzenfeld@uni-due.de