Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a (Template) Haskell library that would allow me to print/dump a few local bindings with their respective names?

For instance:

let x = 1 in putStrLn [dump|x, x+1|]

would print something like

x=1, (x+1)=2

And even if there isn't anything like this currently, would it be possible to write something similar?

like image 259
Wizek Avatar asked Jul 10 '15 20:07

Wizek


2 Answers

I've actually almost solved the problem now. Not exactly what you imagined, but fairly close. Maybe someone else can use this as a basis for a better version. Either way, with

{-# LANGUAGE TemplateHaskell, LambdaCase #-}

import Language.Haskell.TH

dump :: ExpQ -> ExpQ
dump tuple =
    listE . map dumpExpr . getElems =<< tuple
  where
    getElems = \case { TupE xs -> xs; _ -> error "not a tuple in splice!" }
    dumpExpr exp = [| $(litE (stringL (pprint exp))) ++ " = " ++ show $(return exp)|]

you get the ability to do something like

λ> let x = True
λ> print $(dump [|(not x, x, x == True)|])
["GHC.Classes.not x_1627412787 = False","x_1627412787 = True","x_1627412787 GHC.Classes.== GHC.Types.True = True"]

which is almost what you wanted. As you see, it's a problem that the pprint function includes module prefixes and such, which makes the result... less than ideally readable. I don't yet know of a fix for that, but other than that I think it is fairly usable.

It's a bit syntactically heavy, but that is because it's using the regular [| quote syntax in Haskell. If one wanted to write their own quasiquoter, as you suggest, I'm pretty sure one would also have to re-implement parsing Haskell, which would suck a bit.

like image 151
kqr Avatar answered Sep 30 '22 13:09

kqr


TL;DR There is this package which contains a complete solution.

  • install it via cabal install dump

    and/or

  • read the source code

Example usage:

{-# LANGUAGE QuasiQuotes #-}

import Debug.Dump

main = print [d|a, a+1, map (+a) [1..3]|]
  where a = 2

which prints:

(a) = 2   (a+1) = 3       (map (+a) [1..3]) = [3,4,5]

by turnint this String

"a, a+1, map (+a) [1..3]"

into this expression

( "(a) = " ++ show (a)            ++ "\t  " ++
  "(a+1) = " ++ show (a + 1)      ++ "\t  " ++
  "(map (+a) [1..3]) = " ++ show (map (+ a) [1 .. 3])
)

Background

Basically, I found that there are two ways to solve this problem:

  1. Exp -> String The bottleneck here is pretty-printing haskell source code from Exp and cumbersome syntax upon usage.
  2. String -> Exp The bottleneck here is parsing haskell to Exp.

Exp -> String

I started out with what @kqr put together, and tried to write a parser to turn this

["GHC.Classes.not x_1627412787 = False","x_1627412787 = True","x_1627412787 GHC.Classes.== GHC.Types.True = True"]

into this

["not x = False","x = True","x == True = True"]

But after trying for a day, my parsec-debugging-skills have proven insufficient to date, so instead I went with a simple regular expression:

simplify :: String -> String
simplify s = subRegex (mkRegex "_[0-9]+|([a-zA-Z]+\\.)+") s ""

For most cases, the output is greatly improved. However, I suspect this to likely mistakenly remove things it shouldn't.

For example:

$(dump [|(elem 'a' "a.b.c", True)|])

Would likely return:

["elem 'a' \"c\" = True","True = True"]

But this could be solved with proper parsing.

Here is the version that works with the regex-aided simplification: https://github.com/Wizek/kqr-stackoverflow/blob/master/Th.hs

Here is a list of downsides / unresolved issues I've found with the Exp -> String solution:

  • As far as I know, not using Quasi Quotation requires cumbersome syntax upon usage, like: $(d [|(a, b)|]) -- as opposed to the more succinct [d|a, b|]. If you know a way to simplify this, please do tell!
  • As far as I know, [||] needs to contain fully valid Haskell, which pretty much necessitates the use of a tuple inside further exacerbating the syntactic situation. There is some upside to this too, however: at least we don't need to scratch our had where to split the expressions since GHC does that for us.
  • For some reason, the tuple only seemed to accept Booleans. Weird, I suspect this should be possible to fix somehow.
  • Pretty pretty-printing Exp is not very straight-forward. A more complete solution does require a parser after all.
  • Printing an AST scrubs the original formatting for a more uniform looks. I hoped to preserve the expressions letter-by-letter in the output.

The deal-breaker was the syntactic over-head. I knew I could get to a simpler solution like [d|a, a+1|] because I have seen that API provided in other packages. I was trying to remember where I saw that syntax. What is the name...?

String -> Exp

Quasi Quotation is the name, I remember!

I remembered seeing packages with heredocs and interpolated strings, like:

string = [qq|The quick {"brown"} $f {"jumps " ++ o} the $num ...|]
  where f = "fox"; o = "over"; num = 3

Which, as far as I knew, during compile-time, turns into

string = "The quick " ++ "brown" ++ " " ++ $f ++ "jumps " ++ o ++ " the" ++ show num ++ " ..."
  where f = "fox"; o = "over"; num = 3

And I thought to myself: if they can do it, I should be able to do it too!

A bit of digging in their source code revealed the QuasiQuoter type.

data QuasiQuoter = QuasiQuoter {quoteExp :: String -> Q Exp}

Bingo, this is what I want! Give me the source code as string! Ideally, I wouldn't mind returning string either, but maybe this will work. At this point I still know quite little about Q Exp.

After all, in theory, I would just need to split the string on commas, map over it, duplicate the elements so that first part stays string and the second part becomes Haskell source code, which is passed to show.

Turning this:

[d|a+1|]

into this:

"a+1" ++ " = " ++ show (a+1)

Sounds easy, right?

Well, it turns out that even though GHC most obviously is capable to parse haskell source code, it doesn't expose that function. Or not in any way we know of.

I find it strange that we need a third-party package (which thankfully there is at least one called haskell-src-meta) to parse haskell source code for meta programming. Looks to me such an obvious duplication of logic, and potential source of mismatch -- resulting in bugs.

Reluctantly, I started looking into it. After all, if it is good enough for the interpolated-string folks (those packaged did rely on haskell-src-meta) then maybe it will work okay for me too for the time being.

And alas, it does contain the desired function:

Language.Haskell.Meta.Parse.parseExp :: String -> Either String Exp

Language.Haskell.Meta.Parse

From this point it was rather straightforward, except for splitting on commas.

Right now, I do a very simple split on all commas, but that doesn't account for this case:

[d|(1, 2), 3|]

Which fails unfortunatelly. To handle this, I begun writing a parsec parser (again) which turned out to be more difficult than anticipated (again). At this point, I am open to suggestions. Maybe you know of a simple parser that handles the different edge-cases? If so, tell me in a comment, please! I plan on resolving this issue with or without parsec.

But for the most use-cases: it works.

Update at 2015-06-20

Version 0.2.1 and later correctly parses expressions even if they contain commas inside them. Meaning [d|(1, 2), 3|] and similar expressions are now supported.

You can

  • install it via cabal install dump

    and/or

  • read the source code

Conclusion

During the last week I've learnt quite a bit of Template Haskell and QuasiQuotation, cabal sandboxes, publishing a package to hackage, building haddock docs and publishing them, and some things about Haskell too. It's been fun.

And perhaps most importantly, I now am able to use this tool for debugging and development, the absence of which has been bugging me for some time. Peace at last.

Thank you @kqr, your engagement with my original question and attempt at solving it gave me enough spark and motivation to continue writing up a full solution.

like image 34
Wizek Avatar answered Sep 30 '22 11:09

Wizek