Haskell Layout Preprocessor
module StackOverflow where -- yes, the source of this post compiles as is
Go to What to do to make it work if you want to play with this first (1/2 way down).
Skip down to What I would like if I wave a little and you just want to know what kind of help I am looking for.
TL; DR Question summary:
- Can I get ghci to add file name completion to the
:so command that I defined in my ghci.conf ? - Can I somehow define a ghci command that returns the code to compile instead of returning a ghci command, or ghci instead has a better way to hook the Haskell code as a preprocessor with a file extension, so
:l will work as usual , for .hs and .lhs files, but use my handwritten preprocessor for .so files?
Story:
Haskell supports literate programming in .lhs source files .lhs two ways:
- LaTeX style
\begin{code} and \end{code} . - Birds: the code starts with
> , everything else is a comment.
There should be an empty line between the code and the comments (to stop the trivial accidental misuse of > ).
Don't Bird Bird rules sound like StackOverflow code blocks?
Links: 1. .ghci Handbook 2. GHCi haskellwiki 3. Neil Mitchell blogs on :{ and :} on .ghci
Preprocessor
I like to write SO answers in a text editor, and I like to make a message consisting of code that works, but in the end there are blocks of comments or > that I need to edit before publishing, which is less interesting.
So, I wrote myself a preliminary processor.
- If I inserted some ghci stuff as a code block, it usually starts with
* or : - If the line is completely empty, I do not want it to be considered as code, because otherwise I get random code errors following the comment, because I do not see 4 spaces that I accidentally left on the empty line.
- If the previous line was not code, this line should not be either, so we can deal with StackOverflow using indentation for the purpose of composing text outside blocks of code.
At first we donβt know (I donβt know) whether this line is code or text:
dunnoNow :: [String] -> [String] dunnoNow [] = [] dunnoNow (line:lines) | all (==' ') line = line:dunnoNow lines -- next line could be either | otherwise = let (first4,therest) = splitAt 4 line in if first4 /=" " -- || null therest -- so the next line won't ever crash || head therest `elem` "*:" -- special chars that don't start lines of code. then line:knowNow False lines -- this isn't code, so the next line isn't either else ('>':line):knowNow True lines -- this is code, add > and the next line has to be too
but if we know, we should remain in the same mode until we press an empty line:
knowNow :: Bool -> [String] -> [String] knowNow _ [] = [] knowNow itsCode (line:lines) | all (==' ') line = line:dunnoNow lines | otherwise = (if itsCode then '>':line else line):knowNow itsCode lines
Getting ghci to use a preprocessor
Now we can take the module name, pre-process this file and tell ghci to load it:
loadso :: String -> IO String loadso fn = fmap (unlines.dunnoNow.lines) (readFile $ fn++".so") -- so2bird each line >>= writeFile (fn++"_so.lhs") -- write to a new file >> return (":def! rso (\\_ -> return \":so "++ fn ++"\")\n:load "++fn++"_so.lhs")
I used to silently override the command :rso , because my previous certificates used let currentStackOverflowFile = .... or currentStackOverflowFile <- return ... didnβt deliver anything to me.
What to do to make it work
Now I need to put it in my ghci.conf file, i.e. in appdata/ghc/ghci.conf as instructed
:{ let dunnoNow [] = [] dunnoNow (line:lines) | all (==' ') line = line:dunnoNow lines -- next line could be either | otherwise = let (first4,therest) = splitAt 4 line in if first4 /=" " -- || null therest -- so the next line won't ever crash || head therest `elem` "*:" -- special chars that don't start lines of code. then line:knowNow False lines -- this isn't code, so the next line isn't either else ('>':line):knowNow True lines -- this is code, add > and the next line has to be too knowNow _ [] = [] knowNow itsCode (line:lines) | all (==' ') line = line:dunnoNow lines | otherwise = (if itsCode then '>':line else line):knowNow itsCode lines loadso fn = fmap (unlines.dunnoNow.lines) (readFile $ fn++".so") -- convert each line >>= writeFile (fn++"_so.lhs") -- write to a new file >> return (":def! rso (\\_ -> return \":so "++ fn ++"\")\n:load "++fn++"_so.lhs") :} :def so loadso
Using
Now I can save all this message in LiterateSo.so and do wonderful things in ghci, for example
*Prelude> :so StackOverflow [1 of 1] Compiling StackOverflow ( StackOverflow_so.lhs, interpreted ) Ok, modules loaded: StackOverflow. *StackOverflow> :rso [1 of 1] Compiling StackOverflow ( StackOverflow_so.lhs, interpreted ) Ok, modules loaded: StackOverflow. *StackOverflow>
Hurrah!
What I would like:
I would prefer ghci to support this more directly. It would be nice to get rid of the intermediate .lhs file.
Also, it seems that ghci completes the file name, starting with the shortest substring :load , which defines you are actually doing load , so using :lso instead of :so does not trick it.
(I would not want to rewrite my code in C. I also would not want to recompile ghci from the source code.)
Response to the TL; DR questionnaire:
- Can I get ghci to add file name completion to the
:so command that I defined in my ghci.conf ? - Can I somehow define a ghci command that returns the code to compile instead of returning a ghci command, or ghci instead has a better way to hook the Haskell code as a preprocessor with a file extension, so
:l will work as usual , for .hs and .lhs files, but use my handwritten preprocessor for .so files?