-
Notifications
You must be signed in to change notification settings - Fork 1
OutputCapable
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.
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.
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 mMost 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)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
ApplicativeorMonad.
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 p2You 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.
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.5Author: patrick.ritzenfeld@uni-due.de