Thursday, January 14, 2010

Better .ghci files

A few days ago I posted about my plan to use .ghci files for all my projects. I am now doing so in at least five projects, and it's working great. There were two disadvantages: 1) every command had to be squeezed on to a single line; 2) some names were introduced into the global namespace. Thanks to a hint from doliorules, about :{ :} I can eliminate these disadvantages.

Let's take the previous example from HLint's .ghci file:

let cmdHpc _ = return $ unlines [":!ghc --make -isrc -i. src/Main.hs -w -fhpc -odir .hpc -hidir .hpc -threaded -o .hpc/hlint-test",":!del hlint-test.tix",":!.hpc\\hlint-test --help",":!.hpc\\hlint-test --test",":!.hpc\\hlint-test src --report=.hpc\\hlint-test-report.html +RTS -N3",":!.hpc\\hlint-test data --report=.hpc\\hlint-test-report.html +RTS -N3",":!hpc.exe markup hlint-test.tix --destdir=.hpc",":!hpc.exe report hlint-test.tix",":!del hlint-test.tix",":!start .hpc\\hpc_index_fun.html"]
:def hpc cmdHpc

It work's, but it's ugly. However, it can be rewritten as:

:def hpc const $ return $ unlines
[":!ghc --make -isrc -i. src/Main.hs -w -fhpc -odir .hpc -hidir .hpc -threaded -o .hpc/hlint-test"
,":!del hlint-test.tix"
,":!.hpc\\hlint-test --help"
,":!.hpc\\hlint-test --test"
,":!.hpc\\hlint-test src --report=.hpc\\hlint-test-report.html +RTS -N3"
,":!.hpc\\hlint-test data --report=.hpc\\hlint-test-report.html +RTS -N3"
,":!hpc.exe markup hlint-test.tix --destdir=.hpc"
,":!hpc.exe report hlint-test.tix"
,":!del hlint-test.tix"
,":!start .hpc\\hpc_index_fun.html"]

The :{ :} notation allows multi-line input in GHCi. GHCi also allows full expressions after a :def. Combined, we now have a much more readable .ghci file.


gwern said...

Yes, I agree - it's very much easier to read .ghcis using that syntax. Hlint integration now becomes this:

let redir varcmd = case break Data.Char.isSpace varcmd of
(var,_:cmd) -> return $ unlines [":set -fno-print-bind-result",
"tmp <- System.Directory.getTemporaryDirectory",
"(f,h) <- System.IO.openTempFile tmp \"ghci\"",
"sto <- GHC.Handle.hDuplicate System.IO.stdout",
"GHC.Handle.hDuplicateTo h System.IO.stdout",
"System.IO.hClose h",
"GHC.Handle.hDuplicateTo sto System.IO.stdout",
"let readFileNow f = readFile f >>=
\\t->Data.List.length t `seq` return t",
var++" <- readFileNow f",
"System.Directory.removeFile f"]
_ -> return "putStrLn \"usage: :redir var \""
:def redir cmdHelp redir ":redir var \t-- execute , redirecting stdout to var"

--- Integration with the hlint code style tool
:def hlint const $ return $ unlines [":unset +t +s",
":set -w",
":redir hlintvar1 :show modules",
":cmd return (\":! hlint \" ++ (concat $ Data.List.intersperse \" \"
(map (fst . break (==',') . Data.List.drop 2 . snd .
break (== '(')) $ lines hlintvar1)))",
":set +t +s -Wall"]

Much lengthier vertically, but now one can actually hope to read it.

Neil Mitchell said...

I must confess that before I had literally no idea what the hlint integration did, it really is much nicer now.

gwern said...


The :show is reasonably clear; we get output like:

'Main ( ../.xmonad/xmonad.hs, interpreted )'

Note that we *don't* get any of the modules we've loaded like Control.Monad, but just files.

Or, if the file we loaded requires another file, we get something like:

'Mueval.Context ( Mueval/Context.hs, interpreted )
Mueval.ArgsParse ( Mueval/ArgsParse.hs, interpreted )'

Hence, we unlines it, then we use 'break' and 'snd' to discard everything up to the '('. (In retrospect, I probably could've used 'dropWhile'.) The 'drop 2' removes the '(' and the space, then we do the same thing (likewise, 'takeWhile').

At this point, we'll have ["Mueval/Context.hs", "Mueval/ArgsParse.hs"]. the 'concat . intersperse' turns this back into 'Mueval/Context.hs Mueval/ArgsParse.hs' (why didn't I use unlines?), and now we can pass it straight to the '!' command, which will invoke the shell to run 'hlint' on this string.

':redir' is still magic, though. :)

Michael Mounteney said...

"It work's"

Tsck, shame on you !

jinjing said...

Rake is pretty good at doing these kind of thing, also Nemesis if insist to program in Haskell.

Stacy Curl said...

I had some fun creating a continuous testing command:

:def test const $ return $ unlines

:def testMany const $ return $ unlines $
[ ":test"
, ":!sleep 5"
, ":testMany"

Arms with this I just leave a terminal running and I can detect failures in near real time. It would be nice to have the tests run only if the reload picked up something new, then the polling interval could be dropped.

gwern said...

Well... you can use my trick to get the original filename and then check the last-modified time. I'm not sure if you can store that state in ghci, but you could always store it as a $variable or in a . file.