Skip to content
patritzenfeld edited this page Aug 6, 2025 · 2 revisions

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.

Interface

The required function has the following type signature:

parseSubmission :: (Monad m, OutputCapable (ReportT o m)) => String -> LangM' (ReportT o m) Submission

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

  • ReportT is a type from output-blocks that organizes output components. It fills the m in the kind of OutputCapable m => Lang m signatures we’ve seen so far. In fact, pretty much all actual instances of OutputCapable are of the form ReportT OutputType ExecutionMonad. So even though this signature may look more restrictive than those with a plain m, it’s practically equivalent.
  • LangM' is the more general form of LangM and Rated. Whereas LangM has no return value and Rated can only return a score, LangM' can return any type. The types are thus defined: type LangM m = LangM' m () and type Rated m = LangM' m Rational.

What's the Reason for this Weird Type Signature?

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.

Specifying a Parser

To specify the required interface, you need two ingredients:

  • a Parsec parser for the Submission type
  • Some LangM' wrapper function for giving prettier output on errors

Parsec Parsers

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

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

Where 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 Guy

Afterwards, you can use formParser for this imaginary Guy type.

LangM' Error Prettifiers

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 formParser

← Previous: Description | Next: ??? →

Clone this wiki locally