Skip to content

OutputCapable

patritzenfeld edited this page Aug 5, 2025 · 5 revisions

OutputCapable is a Haskell type class developed by our group to abstractly define textual output. It's used to write both task descriptions and feedback texts. You can check out the code in the output-blocks repo.

Why OutputCapable

OutputCapable lets you write output using building blocks without committing to a specific rendering or execution context.

This works because both:

  • the concrete output type (e.g. HTML, plaintext, LaTeX)

  • the execution context (IO, Maybe, etc.)

remain abstract until rendering. This design enables:

  • Separating structure from appearance.
  • Reusing the same output functions across different contexts.
  • Swapping and adding renderers with minimal effort.

Key Types

To express output using OutputCapable, you’ll need two key result types:

  • LangM: Output with no score
  • Rated: Output that includes a score

Both types are parameterized by the execution context (e.g. LangM IO, Rated Maybe). Rather than fixing this parameter, use a type variable constrained by OutputCapable. Being constrained by OutputCapable allows usage of the aforementioned building blocks to construct the output.

The most general type signatures look like this:

myOutput :: OutputCapable m => LangM m

myRatedOutput :: OutputCapable m => Rated m

Writing Sequential Output

Most output consists of multiple paragraphs, headings, images, etc. To compose them, you sequence blocks.

Since OutputCapable is an Applicative, you can write output like this:

myOutput =
  text "This is displayed first" *>
  text "This afterwards"

This works well when you don’t need to retain intermediate results. But if you want to use them, things get messy:

myOutput =
  (+) <$>
    (text "This is displayed first" *> partialScore1) <*>
    (text "This afterwards" *> partialScore2)

Use ApplicativeDo for Cleaner Code

You can simplify this using the ApplicativeDo extension:

{-# language ApplicativeDo #-}

myOutput = do
  text "This is displayed first"
  text "This afterwards"
  pure ()

💡 Why pure ()?

In Applicative do-notation, a final pure is required to wrap the result. Otherwise, the compiler can’t infer whether the block is Applicative or Monad.

In the second example, using ApplicativeDo simplifies things a lot. We no longer need brackets, and each step is indented on the same level. Since arrow bind works too, we can capture values mid-way and use them in the final pure.

{-# language ApplicativeDo #-}

myOutput = do
  text "This is displayed first"
  p1 <- partialScore1
  text "This afterwards"
  p2 <- partialScore2
  pure (p1+p2)

But be careful: Applicative means no dependency between actions. This is only allowed in Monad. Only the final pure may use any intermediate result. This example won’t work, because p2 depends on p1:

{-# language ApplicativeDo #-}

myOutput = do
  text "This is displayed first"
  p1 <- partialScore1
  text "This afterwards"
  p2 <- dependentScore p1 -- ❌ not allowed
  pure p2

Aborts

You can stop the output prematurely by using:

  • refuse: aborts unconditionally
  • assertion: aborts conditionally

These halt the output early and suppress any score entirely. This is different from assigning a score of 0: No score is returned at all, signalling an abort.

⚠️ Don’t use them in task descriptions, only in feedback.

Example Output

This example shows some available primitives and illustrates their usage:

{-# language ApplicativeDo #-}

example :: OutputCapable m => Rated m
example = do
    paragraph $ do
        translate $ do
          english $ "Some English text"
          german $ "Ein deutscher Text"
        code "Monospaced text"
        pure ()


    image "filepath"

    indent $ latex "f \land g \leftarrow h"

    text "This is the same in all languages."

    itemizeM [text "first bullet", text "second bullet"]

    assertion True $ text "this will abort if Bool argument evaluates to False"

    refuse $ text "this always aborts"

    folded True
      (do english "click to open" >> german "hier klicken")
      (text "Contents of the collapsible")
    pure 0.5

← Previous: How Flex Tasks Work | Next: TaskSettings →

Clone this wiki locally