I have been writing lots of Haskell code. Haskell has good support for literate programming which I think is great, so I decided to try it.

Now, for explaining code is really useful to have examples like this one:

[y | x <- [1..10], let y = x +1, odd x] => [2,4,6,8,10]

It would be good if you actually evaluate the result above to avoid potential embarrassments. The simplest way to do it is to fire up ghci, execute the expression, and copy-paste the result.

However, coders hate copy-paste (or maybe it’s just me), and ideally we would like to avoid it. One solution is to use lhs2Tex which has an \eval{} command. lhs2Tex translates only to LaTeX though, so it’s not ideal for generating html or other formats. Pandoc, on the other hand, supports many formats for input and output (including markup), but unfortunately does not support an eval (or similar) function for literate Haskell files. It does support scripting, however, so it might not be too difficult to extend it accordingly.

Let’s start with the necessary modules from the Pandoc library.

import qualified Text.Pandoc as TP
import Text.Pandoc.Walk (walkM)

To do it, we need support for evaluating Haskell code. Fortunately, there is a module for this:

import qualified Language.Haskell.Interpreter as LHI

And a bunch of more imports we are going to need:

import Control.Applicative ((<$>))
import System.Environment (getArgs)

Now what we would like is given code like this:

myfn :: Int -> Int
myfn x = x + 10

And this in the lhs file:

~~~~ {eval=}
myfn 3
~~~~

To produce:

myfn 3 => 13

Indded, this file is a literate haskell source file that uses iteslf to produce the above example. You can find the source in: https://github.com/kkourt/lhseval.

Without knowing anything about either Pandoc or LHI, the following two examples can help to do a quick-and-dirty implementation:

say :: String -> LHI.Interpreter ()
say = LHI.liftIO . putStrLn

doEval :: TP.Block -> LHI.Interpreter TP.Block
doEval cb@(TP.CodeBlock (id, classes, namevals) contents) = do
    r <- LHI.eval contents
    let contents' = contents ++ " => " ++ r
    return $ TP.CodeBlock (id, classes, namevals) contents'


evalBlock :: TP.Block -> LHI.Interpreter TP.Block
evalBlock cb@(TP.CodeBlock (id, classes, namevals) contents) = do
    case lookup "eval" namevals of
        Just f -> doEval cb
        Nothing -> return cb
evalBlock x = do
    return x

evalDoc :: [String] -> TP.Pandoc -> LHI.Interpreter TP.Pandoc
evalDoc modules d = do
    -- force modules to be interpreted (otherwise using this file on itself
    -- fails)
    -- http://stackoverflow.com/questions/7134520/why-cannot-top-level-module-be-set-to-main-in-hint
    let imports = [ "*" ++ m | m <- modules]
    LHI.loadModules imports
    xs <- LHI.getLoadedModules
    -- set them as top-level modules so that all of their functions (not only
    -- the ones exported) can be accessed
    LHI.setTopLevelModules xs
    LHI.setImports ["Prelude"]
    nd <- walkM evalBlock d
    return nd

main :: IO ()
main = do
    -- modules to load
    modules <- getArgs
    docin <- TP.readNative <$> getContents
    docout <- LHI.runInterpreter (evalDoc modules docin)
    case docout of
        Left err  -> error $ "Error in evaluation:" ++ (show err)
        Right res -> putStr $ TP.writeMarkdown TP.def res
    return ()

TODO:

  • Add testing functionality à la Python’s doctest